[rspec-users] Surprising mock behavior

Mark Thomson mark.thomson at ieee.org
Sun Oct 19 01:34:44 EDT 2008


Zach Dennis wrote:
> I'm top posting... I wish I could inline post, but you provided a lot
> of generalizations for how you think things are working on your code,
> but you don't actually post concrete code (the should_receive and
> should_not_receive case you mentioned that wasn't acting like you'd
> expect). Perhaps this will help:
>
> * Mocks are not ordered by default.
>   

Yeah that's consistent with what I'm seeing. I mentioned ordering only 
because it was one of the things that came up in my mind as I was 
searching for a way to understand the different behaviors I was seeing.

> * If you want ordered message expectations on a mock you have to
> explicitly tell them to be ordered.
>   

Really, you can do that? I'm curious about how.

> * You cannot enforce ordering across mocks.
>
> So without explicitly ordering, message expectations can be fulfilled
> in any order. For example the below will pass even though the
> expectation for 1 and 3 does not match the order in when the bar
> method is called:
>
>     file = Object.new
>     file.should_receive(:bar).with("1")
>     file.should_receive(:bar).with("3")
>
>     file.bar "3"
>     file.bar "1"
>
>   
Ok got that.

> Once a message expectation is fulfilled, if the object receives
> another message matching that expectation it will force it to fail.
> For example, the following would fail even though there are two
> expectations for "bar" to be called with "1", which match the two
> calls to file.bar:
>
>     file = Object.new
>     file.should_receive(:bar).with("1")
>     file.should_receive(:bar).with("1")
>
>     file.bar "1"
>     file.bar "1"
>   

This one kind of surprises me, but I understand your point.

> Using should_not_receive and should_receive on the same message gets
> tricky, because you can trick yourself into thinking the thing is
> passing when it shouldn't be. For example the below will fail because
> even though you call file.bar with "2", that matches the expectation
> that file.bar should not be called with "3":
>
>     file = Object.new
>     file.should_receive(:bar).with("1")
>     file.should_not_receive(:bar).with("3")
>
>     file.bar "1"
>     file.bar "2"
>   

Did you mean to say that will *not* fail since "2" will match "not 3"? 
If so that's really good to know. I had imagined that should_not_receive 
was purely a statement about something that doesn't happen, not about 
something different happening. That may explain what I had thought was 
anomalous behavior. I'll have to take another look at that.


> Hopefully this helps clarify some things for you about rspec's mocks.
>
> However...
>
>   
>> object.should_receive :method => :method_name, :with_each_of => [arg1,
>> arg2,... argN]
>>     
>
> I really don't like this. This is hard to read and can lead to
> something that is very convoluted. Can you share the actual code and
> spec you're having issues with so we can try to provide concrete help
> back to you?
>   
Steve's comments helped me understand why this approach isn't necessary.

Mark.



> Zach
>
>
> On Sat, Oct 18, 2008 at 9:49 PM, Mark Thomson <mark.thomson at ieee.org> wrote:
>   
>> Stephen Eley wrote:
>>     
>>> On Fri, Oct 17, 2008 at 2:55 PM, Mark Thomson <mark.thomson at ieee.org>
>>> wrote:
>>>
>>>       
>>>> and then check that the expected messages are being received -
>>>> file.should_receive(:puts).with("a string").once
>>>> file.should_receive(:puts).with("another string").once
>>>>
>>>> Here's what I'm puzzled about. If I don't include the expectation for the
>>>> first string in the spec, the spec will fail the expectation for the
>>>> second
>>>> string. It seems as if "should_receive" is queuing up the messages that
>>>> come
>>>> into the file object and when it tests an expectation it just looks at
>>>> the
>>>> next one in line. If it doesn't match then the expectation will fail.
>>>>
>>>>         
>>> That sounds right to me.  You declared 'file' as a mock.  Mocks are
>>> bratty little children that treat it as an error and throw a tantrum
>>> if you don't give them everything they expect, no more nor less.  (As
>>> contrasted to stubs, which are couch potatoes that will respond if you
>>> call them but don't complain if you don't.)
>>>
>>> So when you create a mock, you need to be very thorough about it.
>>> Every message has to be accounted for somehow.  If it gets more
>>> messages than you tell it, or different messages, it'll error.  If it
>>> doesn't get enough messages, it'll error.  This is correct behavior.
>>>
>>>       
>> Thanks for the explanation Stephen. However, if that is the intention, I'm
>> puzzled by something else - as I said the spec fails if I don't include an
>> expectation for each output message. However it turns out that that's
>> actually not always true. What I've observed is that it behaves differently
>> if I include a "should_not_receive('...')" expectation somewhere in the
>> spec. In that case it seems that I can have as many "file.puts()" in the
>> component being tested as I like without specifying expectations for them,
>> and they pass just fine. In fact I did have such a situation in my initial
>> spec and I think that's what led me to my mistaken understanding of how
>> should_receive is meant to work. But I'm struggling to understand what the
>> rationale is that explains both of these cases.
>>
>> That aside, I also can't help questioning the way the "should_receive"
>> expectation is expressed. Maybe specifying every message sent to the mock is
>> absolutely the right way to test the component. But in view of the general
>> philosophy of expressing expectations in a way that reflects what they
>> actually mean, in my mind this doesn't quite hit the mark. If you say
>> "should receive", the way I read that is that if the object /does /receive
>> what you specify then it should pass. But that's clearly not what happens.
>> Nor is it  an expectation on what will be received next. If that were the
>> case you might call the method "should_next_receive". However, in fact, as
>> long as all messages are accounted for, you can reorder the individual
>> "should_receive" expectations any way you like and the spec will still pass.
>>
>> In fact "should_receive" does not appear to be an expectation on a single
>> message at all (even if you say "should_receive().once", and leaving aside
>> the exception with "should_not_receive" I noted above). I think a better way
>> to think about this is that the total set of "should_receive" calls are
>> _together_ an expectation on the totality of messages received by the
>> object. In view of this, I wonder if a better way to formulate this test
>> might be to say something like -
>>
>> object.should_receive :method => :method_name, :with_each_of => [arg1,
>> arg2,... argN]
>>
>> where arg1, arg2 etc represent the parameters for each individual call to
>> :method_name. i.e. declare the whole of what we _really_ expect the object
>> to receive in a single call to should_receive.
>>
>> You could take this one step further and declare all of the required calls
>> to any number of methods on the object in a single expectation, by making
>> the argument to should_receive an array -
>>
>> object.should_receive [{:method => :method1, :with_each_of => [...]},
>> {:method => :method2, :with_each_of => [...]}, ...]
>>
>> Yeah it gets a little wordy, but if I'm understanding the behavior
>> correctly, this is what we are actually trying to test.
>>
>> Does this make sense?
>>
>> Mark.
>> _______________________________________________
>> rspec-users mailing list
>> rspec-users at rubyforge.org
>> http://rubyforge.org/mailman/listinfo/rspec-users
>>
>>     
>
>
>
>   


More information about the rspec-users mailing list