Rspec & Real World Testing
July 10th, 2008
Rspec is a tasty testing suite for Rails. It’s stubbing can be enhanced by using Mocha, a stubbing framework. There is a spattering of documentation to get you started, but a few controller level items were challenging to test:
- Included modules
- Filtered parameters
- Before filters
- Response codes
- Facebook redirects
And I had to pick up a few model testing tricks too:
- Association testing
- ActionMailer testing
Let’s look at some good approaches for each of these.
Testing Tricks for Rails Controllers
Included modules
1 2 3 |
it "should include AuthenticatedSystem" do controller.class.included_modules.should include(AuthenticatedSystem) end |
Filtered parameters - This basically runs the filtering code over some parameters, then we test the output. Not the most ideal way, but the best I’ve come up with.
1 2 3 4 |
it "should filter credit_cards" do controller.send(:filter_parameters, 'credit_card' => 'nogood')\ ['credit_card'].should == '[FILTERED]' end |
Before filters
1 2 3 |
it "should have a before_filter for login_required" do controller.class.before_filters.should include( :login_required ) end |
Response codes - This is not hard, but I always seem to forget. Test the code, not the status message:
1 2 3 |
it "should return 200 success" do response.code.should == '200' end |
Facebook redirects - Diving into Facebooker and the Facebook platform has been an…uh…engaging experience. One thing that took me a while to realize: Facebooker alters redirect_to to use facebook’s own redirection tag. Don’t test them like normal redirects.
1 2 3 4 5 6 |
it "redirects on facebook based signup" do controller.stubs(:request_is_for_a_facebook_canvas?).returns(true) create_user # Test against the body. Maybe this could even use has_tag. response.body.should =~ /<fb:redirect url="\/home" \/>/ end |
Testing Tricks for Rails Models
Model testing is really very straight ahead once you start feeling comfortable. Some strange points for me were around model associations and ActionMailer.
Model associations - The technique I use is a mashup of association reflection to_hash and a deep_merge method for ruby. deep_merge in general is pretty convenient when testing. Add this to spec/spec_helper.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# Associations to hash module ActiveRecord module Reflection class AssociationReflection def to_hash { :macro => @macro, :options => @options, :class_name => @class_name || @name.to_s.singularize.camelize } end end end end # Hash#deep_merge # From: http://pastie.textmate.org/pastes/30372, Elliott Hird # Source: http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html # This file contains extensions to Ruby and other useful snippits of code. # Time to extend Hash with some recursive merging magic. class Hash # Merges self with another hash, recursively. # # This code was lovingly stolen from some random gem: # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html # # Thanks to whoever made it. def deep_merge(hash) target = dup hash.keys.each do |key| if hash[key].is_a? Hash and self[key].is_a? Hash target[key] = target[key].deep_merge(hash[key]) next end target[key] = hash[key] end target end # From: http://www.gemtacular.com/gemdocs/cerberus-0.2.2/doc/classes/Hash.html # File lib/cerberus/utils.rb, line 42 def deep_merge!(second) second.each_pair do |k,v| if self[k].is_a?(Hash) and second[k].is_a?(Hash) self[k].deep_merge!(second[k]) else self[k] = second[k] end end end #----------------- # cf. http://subtech.g.hatena.ne.jp/cho45/20061122 def deep_merge2(other) deep_proc = Proc.new { |k, s, o| if s.kind_of?(Hash) && o.kind_of?(Hash) next s.merge(o, &deep_proc) end next o } merge(other, &deep_proc) end def deep_merge3(second) # From: http://www.ruby-forum.com/topic/142809 # Author: Stefan Rusterholz merger = proc { |key,v1,v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 } self.merge(second, &merger) end def keep_merge(hash) target = dup hash.keys.each do |key| if hash[key].is_a? Hash and self[key].is_a? Hash target[key] = target[key].keep_merge(hash[key]) next end #target[key] = hash[key] target.update(hash) { |key, *values| values.flatten.uniq } end target end end |
That creates both to_hash and deep_merge. The tests look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
it "should belong to an owner, which is a User" do assoc = Home.reflect_on_association(:owner).to_hash assoc.deep_merge({ :macro => :belongs_to, :options => { :class_name => 'User' } }).should == assoc end it "should have many tree_types, through trees" do assoc = Home.reflect_on_association(:tree_types).to_hash assoc.deep_merge({ :macro => :has_many, :options => { :through => :trees } }).should == assoc end |
This allows for very exact tests on model associations, and useful error messages when they fail. It’s a little too verbose, and there may be something on github that has already moved down this line.
ActionMailer - Definitely test your models separate from your notifier. For instance, test an email is sent upon User creation:
1 2 3 4 |
it "should send an email on user creation" do UserNotifier.expects(:deliver_new_user_notification) User.create( @valid_user_params ) end |
That’s all that’s needed. None of that flushing the unsent email cache you might have seen around the web googling for this. Now test the UserNotifier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
shared_examples_for "mysite.com email" do it "should have a prefix on the subject" do @email.subject.should =~ /[My Site] / end it "should be from noreply" do @email.from.should == ['noreply@mysite.com'] end it "should be multi-part" do @email.parts.length.should be(2) end end describe UserNotifier do describe "when sending a new user e-mail" do before(:each) do @user = User.create( @valid_user_params ) @email = UserNotifier.create_forgot_password(@user) end it_should_behave_like "mysite.com email" it "should be sent to the user's email address" do @email.to.should == [@user.email] end it "should contain the activation_code" do @email.body.should =~ /#{@user.activation_code}/ end end end |
Oh slick. Of course you should flavour to taste, but these tricks are what got me rolling. I hope to post some Facebooker testing tricks as soon as I have some important things like notifications figured nicely out. What did you have to figure out?

Leave a Reply