views:

46

answers:

3

I have an object

class User < ActiveRecord::Base
  has_one :subscription
end

and I have this test:

  it "should increment shipped count when item_shipped" do
    @user.attributes = @valid_attributes
    @user.save

    subscription = mock_model(Subscription)
    subscription.stub!(:item_shipped!)
    subscription.stub!(:user_id)
    @user.subscription = subscription

    lambda{@user.item_shipped!}.should change{@user.shipped_count}.by(1)
  end

But I am getting an error:

1)
Spec::Mocks::MockExpectationError in 'User should increment shipped count when item_shipped'
Mock "Subscription_1113" received unexpected message :[]= with ("user_id", 922717357)
./spec/models/user_spec.rb:29:

I am not sure how to mock this out and I can't seem to find any references to this kind of thing.

+1  A: 

Instead of mocking Subscription, try stubbing out the methods on an actual Subscription instead:

subscription = Subscription.new
subscription.stub!(:item_shipped!)
subscription.stub!(:user_id)
@user.subscription = subscription

Mocks can be brittle. Any call to a mock must be anticipated and declared as an expectation. It doesn't appear that this particular test needs that model mocked in any case.

EDIT: Also remember to declare any return values that the calling class depends on. In your case this might look like:

subscription.stub!(:item_shipped!).and_return(true)
subscription.stub!(:user_id).and_return(@user.id)

etc.

Again, if you're not asserting that a method on your mocked model should be called, then the only thing mocking does here is make your test brittle. Mocks are meant for things like:

subscription.should_receive(:some_method).once

Otherwise you simply need to stub out methods that have undesirable side effects that don't concern your spec.

Dave Sims
Thanks... that doesn't seem to work, but that might be because of the inner workings of item_shipped! I do need to mock subscription because it is being called from within the method I am testing.
phil
Yeah it's hard to say without knowing the particulars of the method. Keep in mind that mocks are intended to be used to create specific expectations. In other words, if your spec is not going to assert that a certain method on a mocked object will be called, then there's no need to mock that object. Just stub out any methods on a real instance of the object that would invoke heavy or otherwise uneeded/undesired operations. As Martin Fowler points out, mocks aren't stubs: http://martinfowler.com/articles/mocksArentStubs.html
Dave Sims
Also keep in mind that if User expects a return value from Subscription in the internals of User#item_shipped!, you'll need to set the return value on the stubbed method (see edit).
Dave Sims
A: 

Setting up associations for tests is made easier with factories: (untested)

Factory.define :subscriber, :class => User do |f|
  f.name "Moe Howard"
  f.association :subscription, :factory => :subscription
end

Factory.define :subscription, :class => Subscription do |f|
end

it "should increment shipped count when item_shipped" do
  @user = Factory.create(:subscriber)
  lambda{@user.item_shipped!}.should change{@user.shipped_count}.by(1)
end

Of course you're not really testing the association here -- you're testing the item_shipped method, which is what you really wanted.

zetetic
Interesting, I will try this. The user item_shipped! method calls subscription (amongst doing other things) which is why I need to mock it.
phil
A: 

change: mock_model(Subscription) to mock_model(Subscription).as_null_object

which will allow for any messages to be sent to the object (assuming this is an acceptable behavior in your case)

brad