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 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.

> This apparently wasn't the case in the last version, where the instance 
> variable would continue to be connected to the same object.

before(:each) blocks have been run before each example since they were first 
introduced, so this is not accurate. Whatever behavior you were seeing was not 
due to RSpec keeping instance variable assignments across examples.

> This might be intended behavior, but it feels like a bug because that 
> memoizing behavior is so common.

Agreed it is common, but the common approach to stubbing things that are 
memoized is to stub the method (in this case my_foo).

> 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.

HTH,
David



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

Reply via email to