On Apr 27, 2011, at 6:40 AM, David Chelimsky wrote:

> 
> On Apr 26, 2011, at 5:39 PM, Matthew Van Horn wrote:
> 
>> 
>> On Apr 26, 2011, at 4:02 PM, Rodrigo Rosenfeld Rosas wrote:
>> 
>>> Ok, now I understand what is your issue.
>>> 
>>>>>> class Foo
>>>>>> end
>>>>>> 
>>>>>> class Bar
>>>>>>   def self.my_foo
>>>>>>     @my_foo ||= Foo.new
>>>>>>   end
>>>>>>   def self.perform
>>>>>>     my_foo.do_something
>>>>>>   end
>>>>>> end
>>>>>> 
>>>>>> describe Foo do
>>>>>> 
>>>>>>   before(:each) do
>>>>>>     @stupid_mock = double('wtf')
>>>>>>     Foo.stub(:new => @stupid_mock)
>>>>>>   end
>>>>>>   
>>>>>>   it "passes here" do
>>>>>>     @stupid_mock.should_receive(:do_something).and_return('value')
>>>>>>     Bar.perform
>>>>>>   end
>>>>>>   
>>>>>>   it "fails here" do
>>>>>>     @stupid_mock.stub(:do_something => 'value')
>>>>>>     Bar.perform
>>>>>>     # double "wtf" received unexpected message :do_something with (no 
>>>>>> args)
>>>>>>   end
>>>>>>   
>>>>>> end
>>>>> 
>>> 
>>>> 
>>> 
>>> The first time Bar.foo is being called, it caches Foo.new. Since you 
>>> stubbed Foo.new to return a mock before running Bar.foo for the first time, 
>>> this mock is cached inside Bar, so any subsequent call will return the 
>>> first created mock object. This obviously won't happen if you run only the 
>>> other spec and that is why it passes in this case.
>>> 
>>> This is not about passing by reference, but it is about caching Bar.my_foo.
>>> 
>>> The only way I can think of RSpec not being affected by this is to 
>>> reloading all classes before each example, which would be both complicate 
>>> and costly.
>>> 
>>> You'll probably have to rethink your testing strategies, avoiding caching 
>>> class methods or not mocking something that could affect them, or 
>>> initializing them first before running the specs...
>>> 
>>> Best regards,
>>> 
>>> Rodrigo.
> 
> The example at the top of this thread fails the same way with rspec-1.3.1, 
> 1.3.2, and 2.5.0. I'm guessing that this was not the real example that you 
> saw passing in rspec-1 and failing in rspec-2, in which case this example 
> probably doesn't trigger whatever differences you were seeing between rspec-1 
> and rspec-2. Are you able to show us the real code?

I'll have to see if I can dig it up later and verify. I've refactored quite a 
bit along the way of this upgrade, so the tests are not all in the same place. 
I am reasonably sure my suite was passing, but I could be wrong. Going from 
memory, in the original version, I stubbed the :do_something method on the 
double before it got memoized, because I wanted that as the default behavior 
except for the one example where I set the expectation. 
Perhaps that is what worked in 1.3.1 and not now.

>> I see now the crux of the issue - mocks are implicitly cleared out before 
>> each example, so even though the same object is being returned, RSpec is 
>> sneakily substituting a doppelganger for @stupid_mock.

> RSpec is doing nothing sneaky. The code in the before hook assigns a new 
> double to the @stupid_mock instance variable for each example, but the my_foo 
> method memoizes the @stupid_mock from the first example at the class level, 
> so it returns that instance for all subsequent requests in the same runtime.

Email is bad for tone - 'sneakily' is meant tongue-in-cheek. Although, it is 
what came to mind when I tried to fix the problem by doing:
before(:each) do
    @stupid_mock ||= double('wtf')

and it still failed. I'm still kind of unclear as to which object is holding 
that instance variable (and when), so it seemed weird to me that it was getting 
reassigned.


>> Because I can't set up the double in a before(:all) block due to the above 
>> behavior, the solution is to add:
>>     Bar.instance_variable_set(:@my_foo, nil) # Forcibly clear Bar's cached 
>> instance
>> to the before(:each) block.
> 
> I'd recommend you just stub the my_foo method instead. You said you prefer to 
> avoid doing that earlier this thread, but I think it's a cleaner solution as 
> it is isolated to the current example. Keep in mind that if the double ends 
> up the memoized value for my_foo, it will remain so for the rest of the 
> suite, whereas if you just stub my_foo for the examples you need the double, 
> it will be limited to those.

Hmm. I guess, I'll re-think my opinions on unit testing. The isolation is a 
good feature, and helps attenuate my fears of stubbing methods of the object 
under test leading to ineffective tests down the road.

-- matt

_______________________________________________
rspec-users mailing list
rspec-users@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

Reply via email to