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

David Chelimsky dchelimsky at gmail.com
Fri Jul 25 09:53:21 EDT 2008


On Fri, Jul 25, 2008 at 8:49 AM, David Chelimsky <dchelimsky at gmail.com> wrote:
> On Fri, Jul 25, 2008 at 8:44 AM, Matt Lins <mattlins at gmail.com> wrote:
>> On Fri, Jul 25, 2008 at 8:40 AM, David Chelimsky <dchelimsky at gmail.com> wrote:
>>>> On Fri, Jul 25, 2008 at 8:15 AM, David Chelimsky <dchelimsky at gmail.com> wrote:
>>>>> On Fri, Jul 25, 2008 at 7:57 AM, David Chelimsky <dchelimsky at gmail.com> wrote:
>>>>>> 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
>>>>>
>>>>> Or you *could* just look at them on line!
>>>>>
>>>>> https://gist.github.com/2438/040f26916032ad864ba51d0d733e16056c77be42
>>>>> https://gist.github.com/2438/0ee4fcaebbafdbdab77dffd5228a9aae92f17191
>>>>>
>>>>>
>>>>>
>>>>>> (this is my first time
>>>>>> checking out gist - wow!).
>>>>>>
>>>>>> HTH,
>>>>>> David
>>>
>>> On Fri, Jul 25, 2008 at 8:30 AM, Matt Lins <mattlins at gmail.com> wrote:
>>>> Yes, gist is great!
>>>>
>>>> Thank you very much for taking the time to look at this.  I like your
>>>> suggestions very much and will use them.  At this point I'm just
>>>> messing around, but I don't understand why this doesn't work.
>>>>
>>>> One more bad implementation if you have time:
>>>
>>> I don't really have much - packing for a week's holiday.
>>
>> Ok, thanks anyway.  Enjoy your holiday. :)
>>
>>>
>>>> http://gist.github.com/2372
>>>>
>>>> I'm removing the constant after each spec runs and redefining it
>>>> before each runs.  The second spec still doesn't pass though, even
>>>> though the constant is defined before each spec.
>>>
>>> Is is the same failure?
>>
>> Yes.
>
> What about when you run them each individually?
>
> spec migration_spec.rb - e 'should get a count of the records'
> spec migration_spec.rb - e 'should find the records'

I actually just did that myself and they both pass separately - so
there is some state-leaking problem. I don't really have the time to
investigate this now, but this is beginning to feel a lot like
"doctor, it hurts when I bang my head against the wall like this ...."

>
>
>>
>>>
>>>> Again, I realize this is horrible, but it should still work, no?
>>> _______________________________________________
>>> rspec-users mailing list
>>> rspec-users at rubyforge.org
>>> http://rubyforge.org/mailman/listinfo/rspec-users
>>>
>> _______________________________________________
>> rspec-users mailing list
>> rspec-users at rubyforge.org
>> http://rubyforge.org/mailman/listinfo/rspec-users
>>
>


More information about the rspec-users mailing list