DaveSouth.org

Email RSS Twitter Delicious

Test Spec and the Double-R

Last year we did a project with substantial business logic requirements. The Rails testing framework saved us. We could create tests for all the different uses of the system and ensure that these rules were always followed. As wonderful as it was, there was a bitter aspect to Rails testing — fixtures.

Using fixtures to test the database layer for an individual model was fine. However, we built a class that would manipulate a half-dozen models. Ensuring that all the fixtures were perfectly synchronized was beyond painful. If we added a fixture to test some small new feature, the additional fixture would ripple through the tests causing failures on code that had nothing to do with the new feature.

We needed something better.

Test/Spec

I looked long and hard at RSpec. It is always a bit behind the Rails Edge and uses an entirely different syntax for testing. I know a holy war is going on about should.equal versus assert_equal, but for me, I prefer the terse Test Unit syntax.

One aspect of RSpec I did like – and desperately wanted – is the ability to set up different parameters for different sets of tests.

The answer was test/spec and test/spec/rails. This added context abilities to test unit with the benefits of setting up and tearing down test parameters on a granular level. It also used existing test methods like assert_equal and assert_difference. And it tracked the Rails Edge, perfectly, because it only adds features to Test Unit.

Yes, I know it supports should.equal and it’s cousins, but who cares.

Mocking

Fixtures and database testing are critical for a single model. The last thing you need is unpredictable returns from the actual database. But once you move up a level – say the controller or a separate class – hitting the database for every test gets very old.

At the controller level – or in our previous example, a separate class manipulating many models – we only need enough “data” to validate our particular business need. The actual method and returns of a particular search should be moved into the individual models and tested there. At this higher, more abstract, logic level we just need a select combination of data to prove the logic.

Flexmock and Mocha allow this simple idea. Your controller would ask for data, but the mocking object will simulate the model’s response and return just enough data to make the test work correctly. This skips the problem of a model spewing back a whole bunch of fixtures that are not necessary or are recently added. We’re testing the logic, not the data.

We settled on using Flexmock. The results were fine. But the mocking code was wordy.

For a flexmock object you would write:

  flexmock(Story).should_receive(:find_with_permalink).once.with('my-article-permalink').and_return(stories(:my-article))

Yikes.

Surely it could be shorter and still be just as clear.

RR

While attending the Hackfest at the Mountain West Ruby Conference, a programmer showed me the answer – Double-R from Pivotal Labs. It has a terse, clear syntax.

Using Double-R the same mock would be written:

  mock(Story).find_with_permalink('my-article-permalink') { stories(:my-article) }

Look familiar? The model code it’s mocking is:

  Story.find_with_permalink('my-article-permalink')

Wonderful!

What about a larger data set.

  mock(Story).find_after_date('2008-01-01') { stories( :my-article, :your-article, :this-article) }

I get a list of three stories back from the mock. Just enough to test my logic and not a bit more.

RR and Test Unit

RR was written to work with RSpec or Test Unit. Loading the RR TestUnit adapters directly into test_helper caused all my tests to fail. Instead, I require the RR library in test_helper.

Add this to test helper:

  require 'rr'

Then for each test that needs to use RR, I add this line right after the test_helper call (usually line 2):

  class Test::Unit::TestCase; include RR::Adapters::TestUnit; end

Test/Spec and RR

A snippet from our press class test:

require File.dirname(__FILE__) + '/../test_helper'
class Test::Unit::TestCase; include RR::Adapters::TestUnit; end

context 'Press tag chain traversal' do
  setup do
    login_as :dave
  end

  it 'should return all stories with empty tags' do
    mock(Story).find_everything { stories(:products, :software) }
    press = Press.new
    assert_equal stories(:products, :software), press.stories
  end

  it 'should return story using permalink' do
    mock(Story).find_with_permalink('software') { stories(:software) }
    press = Press.new %w(products software)
    assert_equal stories(:software), press.story
  end
end

The actual implementation of “find_with_permalink” and “find_everything” is in the Story model. Press is just an aggregator class and it just needs to know that it 1) actually called the right command and 2) received valid information.

Stubs

Doing stubs in RR is just as easy as mocks.

  stub(Story).find_everything { stories(:products, :software, :widget) }

Stubs are different from mocks. Mocks must be called or the test fails. Stubs are only there for convenience and if they are not called, the test will still pass.

And speaking of test failures. When a test fails because the mock wasn’t called, flexmock would generate a difficult to read error. RR’s errors are far more clear so debugging is faster.

Notes

To install RR:

  gem install rr