Rspec notes from the trenches

Courtenay : June 16th, 2007

A frequent topic in #rspec is "when should you mock?" I'm not much of a theorist, but recently I tend to be testing at a higher level than before; that is, more abstract.

How much to test?

What I used to tell people was, "Could someone else reproduce your code from the spec?"

However, I'm starting to think the key question should be "Could someone else reproduce your application from the specs?"

It's a key difference, and one I'm really only still learning. That is, the implementation should not really matter.

When to mock?

When I was learning about mocks in rails, I was told there were two reasons not to use the actual models in controller specs:

  1. the law of demeter; or, more simply test the models in their place and use a consistent interface to them that doesn't change
  2. speed

When you're in the midst of a controller spec, you want to mock away everything that isn't in that controller. So if your code is using senddata, or User.find, or even a beforefilter, stub that method out and return a mock, because it should be tested elsewhere.

However, in some cases, it's more useful to return a real live object. The obvious case for me is when you're rapidly developing an object and the upkeep of the mocks and the object's interface starts becoming tedious: imagine a user model where you change the name of a field, and then have to go through your code hunting for user mocks to change that field. Yuck.

For example, here's an extraordinarily lazy spec I just hammered out:

it "should successfully load client_product if id specified" do
  @store.should_receive(:client_products).and_return products = mock('proxy')
  products.should_receive(:find_by_permalink).with('1').and_return ClientProduct.new

  get :show, :id => '1'
end

I'm actually returning an instance of the product model, rather than just mocking out the fields.

The alternative way to do it is

it "should successfully load client_product if id specified" do
  @store.should_receive(:client_products).and_return products = mock('proxy')
  products.should_receive(:find_by_permalink).with('1').and_return product = mock_model(Product)

  product.stub!(:field).and_return('value') # repeat as necessary and upkeep. :(
  get :show, :id => '1'
end

Finally, notice I use a find_by_permalink method there, which prevents the controller from seeing too closely into the model. I plan on never changing that interface.

8 Responses to “Rspec notes from the trenches”

  1. Rupert Says:

    I totally agree with your sentiments here…. I’m just trying to get to grips with all this agile stuff and mocking and have the same concern.

    It seems crazy to me that if I change the interface to a model that I have to then find all the places where I’ve mocked it (using that interface) and change them there too to make sure the change hasn’t broken something else.

    I can’t get my head around whether this is a better or worse situation that having lots of potentially cascading failures and dependancies between tests.

    I feel like it would be nice to be able to run the specs in two modes, either :

    • using the mocks as they are defined
    • or using the underlying model and ignoring the mocks (but still checking the calls to the model - as in checking return values).

    Something along those lines would seem to offer the advantages of either approach but I’m too much of a novice to know if this is either practical or sensible.

    Cheers

    Rupert

  2. Chris Says:

    “Could someone else reproduce your application from the specs?”

    This is an awesome way of thinking about specs and testing in general.

  3. Carl Says:

    I agree with Chris, what a great way of describing specs.

    Ruport, that is an interesting idea.

    What if your were only able to mock existing methods? That way if you change the interface, all of the mocks that are dependent on your interface could report that they are no longer valid. Perhaps your tests would not fail (so they don’t cascade), but they could throw warnings.

    Cheers Carl

  4. Dan Says:

    An even simpler solution would be to finally get decent integration test support in rspec.

    That’s one of the biggest reasons we switched back to Test::Unit from Rspec.

    Unit tests alone simply aren’t enough.

  5. Rupert Says:

    Carl: I think you’d still want to be able to mock non-existant methods so you could still test properly before all your models are built or completed.

    However, if you had a central config file somewehre that defined what to do when you come across a mock_model(MyWonderousModel) statement. You could tell it to use the mocks as mocks before you’ve built the MyWonderousModel. Then when you’ve built the MyWonderousModel you could tell it to use the actual model when running tests.

    This would seem to potentially allow things to be built in a separated/mocked way (which is good), but would also allow integration testing to be carried out with the minimum of effort by telling it to use the config file or not to switch off certian mocks.

    Not really given it much thought in depth mind you so there’s probably some bad things lurking in this idea!!

  6. Carl Says:

    Rupert: That makes sense. I was primarily addressing refactoring so that when you change a method, your mocks yell at you saying that the changed method doesn’t exist (warning without failing). This is my biggest concern with mocks: maintaining tests while changing interface.

    Jay Fields addresses this here: http://blog.jayfields.com/2007/05/testing-replace-mock-with-stub.html

  7. court3nay Says:

    Rupert: You do know that test::unit and rspec can co-exist, right? I combine integration tests (only) and specs.

  8. Ryan Bates Says:

    I have been moving away from mocks for the same reasons mentioned in this post: it makes the specs/tests too rigid. I wrote more about this on the rails forum:

    http://railsforum.com/viewtopic.php?id=6528

    I feel a little dirty abandoning mocks in this way, but I couldn’t see a better alternative. Maybe I just wasn’t using them properly.

Sorry, comments are closed for this article.