Wednesday, May 30, 2007

Mocking and stubbing with Mocha

Here is a nice idiom for unit testing in Rails with the Mocha library. Mocha is fantastic tool for testing, and its is the one we use at my job for mocking with expectations. There are two methods I wasn't aware of, mock and stub. They're very easy to use and are especially handy for situations where you might instantiate another ActiveRecord object temporarily.

For instance, let's say I'm testing messages, and I want to verify that a given call properly delegates to a subordinate object of another type:
 def test_read_by
@message.message_recipients.expects(:find_by_recipient_id).
with(@profile.id).
returns(MessageRecipient.new(:status_read => true))
assert @message.read_by?(@profile)
end

This is pretty straightforward, but seeing that other class name in there bothers me. I've had trouble with over-specific tests before. Here's a version using the stub method:
 def test_read_by
@message.message_recipients.expects(:find_by_recipient_id).
with(@profile.id).
returns(stub(:is_read? => true))
assert @message.read_by?(@profile)
end

This is similar, but there are a couple of important differences. First, I no longer have to hardcode the class name of the subordinate object, which may change later. More importantly, I can mock the friendlier external interface of MessageRecipient without having to know anything else about the object or its contents. Other than that it's exactly the same as the previous test.

Now let's try it with mock:
 def test_read_by
@message.message_recipients.expects(:find_by_recipient_id).
with(@profile.id).
returns(mock(:is_read? => true))
assert @message.read_by?(@profile)
end

Once again, very little difference. The call to stub is replaced by a call to mock with the same arguments. However if you run this test you'll see an extra assertion being checked. This is because mock will verify that every mocked method was called, as though you'd used expects. This means that in addition to the benefits conferred by using a stub object, you're now verifying that the object was used as intended.

0 comments: