Rspec & Real World Testing

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

  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.

  it "should filter credit_cards" do
    controller.send(:filter_parameters, 'credit_card' => 'nogood')\
      ['credit_card'].should == '[FILTERED]'
  end

Before filters

  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:

  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.

  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:

# 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:

  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:

  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:

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?