Written by: steve ross on May 3rd 2007

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:

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:

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
      zoo
      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.