Rspec & Real World Testing
Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.
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?