RR cheatsheetEdit

Proxying

mock proxy

Confirm a message was sent, but pass it through to original implementation:

# call to be tested:
@forums = Forum.all

# spec:
mock.proxy(Forum).all

This is useful where later code depends on the return value of the mocked call, and without proxy we would otherwise have to manually specify an object to be returned, something which we are not interested in doing if our intention in the spec is only to confirm that Forum.all was sent:

# lengthier manual set-up:
mock(Forum).all { [Forum.make!] }

Chaining

top-level stub, second-level mock

Here we want to confirm that a message was sent at the second level:

# call chain to be tested:
@forum = Forum.find(params[:id])
@forum.to_json(...) # <== want to test this call

# spec:
@forum = Forum.make!
stub(Forum).find(@forum.id).mock(@forum).to_json(anything)

Note that we can also confirm at both levels of the chain:

mock(Forum).find(@forum.id).mock(@forum).to_json(anything)

But in practice if the first level stub is not exercised, the second level mock will fail anyway, so it is not necessary to use mock at both levels, unless it is pertinent to the spec to test the exact params passed into the top-level method.

top-level stub, second-level stub

Here we want to inject a return value at the second level:

# call chain to be tested:
@forum = Forum.find(params[:id])
if @forum.update_attributes(params[:forum])
  ...
else
  ... # <== want to test this code path
end

# spec:
@forum = Forum.make!
stub(Forum).find(@forum.id).stub(@forum).update_attributes(anything) { false }

We can change either of these stubs for mocks if we want an exception to be raised if the message isn’t sent:

stub(Forum).find(@forum.id).mock(@forum).update_attributes(anything) { false }
mock(Forum).find(@forum.id).mock(@forum).update_attributes(anything) { false }
mock(Forum).find(@forum.id).stub(@forum).update_attributes(anything) { false }

And we can use a more lax double for the top-level if the circumstances allow it (ie. no other calls to find are expected):

stub(Forum).find.stub(@forum).update_attributes(anything) { false }

Another example:

# in controller
@tag = Tag.find_by_name! params[:id]
if @tag.update_attributes params[:tag]
  ...
else
  ... # <= want to test this code path
end

# in spec
stub(Tag).find_by_name!(tag.name).stub!.update_attributes { false }

multi-level stubbing and mocking

# given this method:
def new
  @user   = User.new
  @email  = @user.emails.build # <== want to confirm the calls to "emails" and "build"
end

# spec:
stub(User).new.mock!.emails.mock!.build

Note that there are other specs we can (and should) write which complement this one:

describe '#new' do
  it 'assigns a new user' do
    get :new
    assigns[:user].should be_kind_of(User)
    assigns[:user].should be_new_record
  end

  it 'assigns a new email' do
    get :new
    assigns[:email].should be_kind_of(Email)
    assigns[:email].should be_new_record
  end

  it 'associates the email with the user' do
    stub(User).new.mock!.emails.mock!.build
    get :new
  end

  it 'renders users/new' do
    get :new
    response.should render_template('users/new')
  end

  it 'succeeds' do
    get :new
    response.should be_success
  end
end

Alternatives to multi-level chaining

Given a method like this:

def create
  @user = User.new params[:user]
  @email = @user.emails.build :address => @user.email
  if @user.save
    ...
  else
    ... # <= want to test this code path
  end
end

Rather than mocking or stubbing the User.new call, or touching the @user.emails.build chain, we can use instance_of to just target the save call and forget about the rest:

stub.instance_of(User).save { false }

General observations

Sometimes doubles can interact in bizarre and unpredictable ways, so it is absolutely important to check that your specs properly fail when the message expectations are violated. That is, you must ensure you’ve got "red" specs in place before you add or correct the implementation to turn them "green".

Example

Given this code:

@paginator = RestfulPaginator.new params, Snippet.published.count, snippets_path, 10
@snippets = Snippet.recent.offset @paginator.offset

Among, other things, I wanted to confirm that the correct offset parameter was being passed in to the Snippet.recent.offset method.

I would have expected this kind of mock to work:

stub(Snippet).recent.mock!.offset(10)

But, curiously, it does not (it accepts any parameters rather than the required 10). I tried alternative syntaxes and methods of chaining, but in the end the only way I could get it to work correctly was to stub out the other call on the Snippet class as well:

stub(Snippet).published.stub!.count { 100 }
stub(Snippet).recent.mock!.offset(10)

This is puzzling because the published call is not at all related to the recent call (ie. published does not call recent internally), so I can’t see how or why it might interfere with the other stub.

Whatever the cause, the lesson is clear: make sure your specs fail first, otherwise you may end up with an implementation that is incorrect but your mocks won’t alert you to the problem.

Related