Testing: Mocking, Stubbing, and Rails, Oh My!
Written by steve on 05-03-2007 at 03:54 PM
Ruby is a great language and the various test frameworks that support it make writing solid code easier. But… Ruby is a magical language and testing magical things can be a bit … shall we say … interesting. One goal of testing is to predict the future and then watch happily when your prediction comes true. That means maintaining tight and relevant control over the kind of data you are testing with and how your program interacts with that data.
Enough of the totally abstract, here’s a more concrete example. When testing Rails controllers, it’s best not to confuse your tests with ActiveRecord behavior, even though you are almost certainly using ActiveRecord to retrieve your data. The typical Test::Unit methodology is to:
- Load up a bunch of fixtures with fake data—one fixture for each unit under test
- Specify fixtures :foo for each unit that needs foo’s
- Drink copious amounts of coffee while the database is emptied, populated with the fixture data, rolled back, and so on, and so on.
This works great because your controller is working with real ActiveRecord objects. However, it relies on your having put reasonable test data in the fixtures—fixtures that can quickly become out of sync with your model and your test requirements.
Mocking and Stubbing
A better way to approach much of your testing is to use mock objects or to stub certain functionality. I’m not sure I have my head wrapped completely around this and opinions vary on how best to approach it. Here’s how I think of mocking and stubbing.
Mock when you know what you expect your controller (from our example) to do with an object (like call it once with three arguments) Stub when you don’t much care what the controller does but you want to provide the controller certain data
So, for example, say we have controller code such as:
def display_people
@person = Person.find_by_name('Bob Smith')
end
Straightforward. Now let’s look at how to test this. Assuming the Mocha mock framework:
def test_display_people_should_find_bob_smith
Person.stubs(:find_by_name).returns({:name => 'bob smith', :address => '123 Main St.'})
get :display_people
assert_not_nil assigns(:person)
end
Hardly earthshaking code, but it illustrates one key point: You don’t even need a functioning model to test a controller. In fact, you probably want to isolate the tests such that such reliance doesn’t crop up in your code. Beyond testing a controller method in the absence of a real model, we’ve also accomplished a few other neat tricks:
- eliminated all the database setup/takedown
- sped up the test run
Now for the fun part. Let’s modify the controller to have a success and a conditions path. First the test:
def test_display_people_should_find_bob_smith
Person.stubs(:find_by_name).returns({:name => 'bob smith', :address => '123 Main St.'})
get :display_people
assert_not_nil assigns(:person)
assert_nil flash[:error]
end
def test_display_people_should_set_flash_if_bob_goes_missing
Person.stubs(:find_by_name).returns(nil)
get :display_people
assert_nil assigns(:person)
assert_equal 'no bob smith here. look next door', flash[:error]
end
Ok, if you’re following along, you have your test failing because of the new feature you are about to add. Let’s fix the controller to provide that feature.
def display_people
@person = Person.find_by_name('Bob Smith')
flash[:error] = 'no bob smith here. look next door' unless @person
end
All of this works equally well in rSpec, which I prefer:
describe "A DummyController displaying people" do
it "should find bob smith" do
Person.stub!(:find_by_name).and_return({:name => 'bob smith', :address => '123 Main St.'})
get :display_people
assigns[:person].should_not be_nil
flash[:error].should be_nil
end
end
it "should set a flash if bob smith goes missing" do
Person.stub!(:find_by_name).and_return(nil)
get :display_people
assigns[:person].should be_nil
flash[:error].should eql('no bob smith here. look next door')
end
end
Mocha and rSpec’s built-in mock frameworks have similar syntax and capability, but if you have to settle on one, it could be that Mocha – because it works across testing frameworks – is the better choice. Fortunately, it’s as simple as this.
In Test::Unit
require 'mocha'
In rSpec spec_helper.rb
config.mock_with :mocha
Note that any time you change your version of rSpec and do a script/generate rspec, you will have to readd this to your spec_helper.
0 comments