Stub Queries and Expect Commands

In Making a Mockery of ActiveRecord, a single test has multiple expectations set up for accessor methods…

specify "should create EmailMessages and Subscriptions when include_subscribers is true" do
  @message.include_subscribers = true
  @message.should_receive(:lists).and_return([@list])
  @message.should_receive(:campaign).twice.and_return(@campaign)
  @list.should_receive(:people).and_return([@person])
  @person.should_receive(:campaigns).and_return([])

  @person.should_receive(:subscriptions).and_return(@subscriptions)
  @subscriptions.should_receive(:create).and_return(nil)
  @campaign.should_receive(:people).and_return([])

  message.generate # I think this line is missing from the original test
  @message.should_have(1).email_messages
end

I’ve found Nat Pryce’s rule of thumb – stub queries and expect commands – to be very useful in reducing the brittleness of tests (see Yoga for Your Unit Tests).

A query in this context is a method which does not change the state of the object on which it is called. The accessor methods definitely fall into this category, so I would stub them, not set expectations for them. The one command method in the above example which merits an expectation is the call to create on @subscriptions.

However, I prefer to use in-memory ActiveRecord objects instead of mocks or stubs wherever possible. So I’d write something more like this (using Test::Unit and Mocha)…

def test_should_create_email_messages_and_subscriptions_when_include_subscribers_is_true
  subscriptions = mock()
  person = Person.new(:campaigns => [])
  person.stubs(:subscriptions).returns(subscriptions)
  list = List.new(:people => [person])
  campaign = Campaign.new(:people => [])
  message = Message.new(:lists => [list], :campaign => campaign)
  message.include_subscribers = true

  subscriptions.expects(:create)

  message.generate
  assert_equal 1, message.email_messages
end

I haven’t actually run this – so it’s quite likely there are errors in it, but you should get the general idea. The person.stubs line is necessary to avoid the type checking that Luke Redpath mentions in his comment.

More thoughts here.