[rspec-users] Problems with mock assigned to a constant

David Chelimsky dchelimsky at gmail.com
Fri Jul 25 08:57:39 EDT 2008

On Fri, Jul 25, 2008 at 12:34 AM, Matt Lins <mattlins at gmail.com> wrote:
> On Fri, Jul 25, 2008 at 12:25 AM, Scott Taylor
> <mailing_lists at railsnewbie.com> wrote:
>> On Jul 25, 2008, at 1:15 AM, Matt Lins wrote:
>>> On Thu, Jul 24, 2008 at 11:47 PM, Scott Taylor
>>> <mailing_lists at railsnewbie.com> wrote:
>>>> On Jul 25, 2008, at 12:32 AM, Matt Lins wrote:
>>>>> I suppose the way I'm defining the stubs, differs from what Dave is
>>>>> doing in his example.
>>>>> I assumed that:
>>>>> MyModel = mock('MyModel Class', :count => 1)
>>>>> was the same as:
>>>>> MyModel.stub!(:count).and_return(1)
>>>> Nope.  Not even close.  Here's an equivalent of the first form:
>>>> Object.send :remove_const, :MyModel
>>>> MyModel = <a mock object>
>>>> and here's the second form:
>>>> MyModel.instance_eval do
>>>> def count
>>>>  1
>>>> end
>>>> end
>>>> (or:)
>>>> MyModel.class_eval do
>>>> class << self
>>>>  def count; 1; end
>>>> end
>>>> end
>>>> Scott
>>> But the stubs are defined the same way in both occurrences, no?
>>> MyModel = mock('MyModel Class', :count => 1)
>>> By passing {:count => 1} to +stubs_and_options+ I should have defined
>>> stubs on the mock object.  I'm using it as a shortcut for this:
>>> MyModel = mock('MyModel Class')
>>> MyModel.stub!(:count).and_return(1)
>>> If those example aren't doing the exact same thing I guess I'm a
>>> little baffled (or maybe just need to go to sleep).
>> The first one is redefining the constant 'MyModel'.  The second one is just
>> redefining a class method (the constant isn't changing - it's remaining
>> whatever it was before - say, a class)
>>>>> But, I'm starting to think they are not.  I haven't looked at the
>>>>> rSpec internals to verify, other than the parameter name:
>>>>> stubs_and_options+ lets you assign options and stub values
>>>>> at the same time. The only option available is :null_object.
>>>>> Anything else is treated as a stub value.
>>>>> So, is this problem?
>>>> Yeah - so here are two related, but not equivalent ideas: mock objects,
>>>> and
>>>> stubs.  A stub is just a faked out method - it can exist on a mock object
>>>> (a
>>>> completely fake object), or on a partial mock (i.e. a real object, with a
>>>> method faked out).  mock('My mock") is a mock object,
>>>> MyRealObject.stub!(:foo) is a real object with the method foo faked out.
>>>> What is the difference between a mock object and a fake object?  A mock
>>>> object will complain (read: raise an error) any time it receives a
>>>> message
>>>> which it doesn't understand (i.e. one which hasn't been explicitly
>>>> stubbed).
>>>> A real object will work as usual.  (A null object mock is a special type
>>>> of
>>>> mock - one which never complains.  For now, you shouldn't worry about
>>>> it).
>>> Ok, I get what you saying, but as I understand it I am explicitly
>>> stubbing out the methods on the _mock_ object and it's still
>>> complaining.  If +stubs_and_options+ isn't stubbing, then what is it
>>> doing?
>> That's right - the hash to the mock method is a shorthand.  So these two are
>> equivalent:
>> my_mock = mock('a mock')
>> my_mock.stub!(:foo).and_return(:bar)
>> AND:
>> my_mock = mock("a mock", {:foo => :bar})
>> Scott
>> _______________________________________________
>> rspec-users mailing list
>> rspec-users at rubyforge.org
>> http://rubyforge.org/mailman/listinfo/rspec-users
> Ok, then would you still insist that:
> This:
> http://gist.github.com/2372
> Should produce this:
> # spec migration_spec.rb
> .F
> 1)
> Spec::Mocks::MockExpectationError in 'Migration should find the records'
> Mock 'MyModel Class' received unexpected message :count with (no args)
> ./migration.rb:14:in `run'
> ./migration_spec.rb:19:
> Finished in 0.009435 seconds
> --------------------
> And like I said earlier, this code passes both examples with FlexMock(
> if you simply replace mock with flexmock and uncomment the code in
> spec_helper, of course you need the flexmock gem)

I can't speak for why it's passing in Flexmock, but I can explain why
it's failing in rspec.

RSpec clears out all stub methods and message expectations at the end
of each example. In this case, the stub on count is defined in a
before(:all) block, which is only executed once, before all the
examples are run (perhaps before(:any) would be a more clear
expression of this?). After the first example is executed, that stub
goes away. So when the mock receives the :count message in the second
example, it's not expecting it (which is exactly what it's telling
you). If you run the second example by itself (spec migration_spec.rb
-e "should find the records") it will pass.

You can solve the immediate problem by removing the stubs from the
initial declaration of the MyModel constant and moving them to a
before(:each) block so they get set before each example.

Another option is to set :null_object => true. That will tell the mock
to ignore unexpected messages, however the stub on find might still
need to move to before(:each) because it returns something.

Also - this code creates instance variables that get used across
examples. If something happens in the first example to change the
state of @record, you're going to get the same object in the second
example and it becomes a challenge to understand what's happening when
there are failures in the second example.

I don't use before(:all) blocks this way for exactly this reason. They
are run only once, and can cause a lot of confusion because they leak
state across examples. The way I usually go about something like this
is to create a simple empty class:

class MyModel; end

And then set expectations on it before(:each) example.

You can get the gist of what I'm talking about here:
http://gist.github.com/2438 - I've got two different approaches in two
separate commits, so grab the repo to see both (this is my first time
checking out gist - wow!).


More information about the rspec-users mailing list