Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Add allow(...).to receive_message_chain #467

Merged
merged 1 commit into from
Nov 18, 2013

Conversation

fables-tales
Copy link
Member

First pass. Comments on the back of a PR :)

end
end
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an awful small class to get its own file. Can we find another place for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

framework.rb?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not leave it in receive.rb?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm in favor of leaving it in receive.rb.

@myronmarston
Copy link
Member

I don't see anything here for:

expect(k).to receive_message_chain(...)
allow_any_instance_of(k).to receive_message_chain(...)
expect_any_instance_of(k).to receive_message_chain(...)

@recorded_customizations = []
end

[:and_return, :and_throw, :and_raise, :and_yield].each do |msg|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're missing and_call_original.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stub chain does not work with and_call_original:

https://gist.github.com/samphippen/7458414

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you found a bug in stub_chain that we should fix. In your example, a is not test double. Also we should fix the wording so it says "pure test double" vs "partial double" rather than using mock in the message.

@myronmarston
Copy link
Member

Also, what about block implementations? I don't see any specs showing that working.

@fables-tales
Copy link
Member Author

Also, what about block implementations? I don't see any specs showing that working.

I've now added these.

@@ -2,7 +2,7 @@
require 'delegate'

describe "and_call_original" do
context "on a partial mock object" do
context "on a partial double" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find "partial double" confusing here, how about "on a method double" instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you find confusing about the term "partial double"? "MethodDouble" is an internal class within rspec-mocks, but it's not a concept we expose publicly and this context isn't really about that...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test double to me is an entirely fake object. A method double indicated we're faking only one method. I thought method double was a more widely known concept than just rspec?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I've never heard it used outside of RSpec, but I don't have much exposure to using test doubles outside of an RSpec context.

Anyhow, in #444 the term we decided on for the new config option was "partial double", so we should be consistent here. If we want to change how we refer to the concept, we can, but that is a bigger issue that we should address in a separate PR if we do address it. For now, being consistent with that naming is best, I think.

@JonRowe
Copy link
Member

JonRowe commented Nov 14, 2013

Good work @samphippen! Will need a change log entry of course and I'd like to see these commits squashed?

Matchers::ReceiveMessages,
Matchers::ReceiveMessageChain,
]
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be moved into a constant? As it is implemented now, every time it is called in allocates a new array object, which seems wasteful, given that conceptually, it's a constant.

end
end
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some other things to consider testing here:

  • expect().to receive_message_chain -- this is a feature that the old syntax lacked, but I don't see a reason not to support it. It's one thing I like about the new expect/allow + matcher system: we easily get feature parity for both message expectations and message allowances. That said, if it's a bunch of work to get this to work, I'm not opposed to disallowing it....but I'd want a test here that documents that fact. As it stands now, it's completely undocumented what happens when you use expect with receive_message_chain.
  • receive_message_chain("foo.bar.bazz") -- this is one of the things stub_chain supports and we may as well support it. I think it should "just work" but a test to confirm and document would be great.
  • receive_message_chain(:foo, :bar, :bazz => "return value") -- this works for stub_chain (see stub_chain_spec.rb) and should either work here or be documented as not working.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any instance expect proved to be difficult, everything else exists now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! One other thing I forgot to mention: the negative case (e.g. allow().not to receive_message_chain). I think you already have code to prevent this but specs to document the behavior would be good. Given you already have the code in place for it, it'd be good to "break" it somehow to confirm the tests you add for this can properly fail with a useful failure message.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a spec for this later :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@fables-tales
Copy link
Member Author

@myronmarston I have to get on a plane now, but I think I've addressed all the feedback. Can you take another look through this and tell me what you think when it goes green?

Thanks 😄

I'll add a changelog in ~8 hours when I get off the plane.

fables-tales pushed a commit that referenced this pull request Nov 15, 2013
@fables-tales
Copy link
Member Author

👍 I think it's organized better now. Do you like it better or worse?

I'm down with this. I particularly like how chain_on got pushed into one place.

@fables-tales
Copy link
Member Author

@myronmarston I've updated based on your feedback.

I've noticed that if you've set an expectation/allowance before you created a chain, and that expectation/allowance returns nil you get a stubbing method on nil warning. I'm not sure how you feel about that, but I'm pretty sure it's an inevitable consequence.

@fables-tales
Copy link
Member Author

@myronmarston can you take a look at this please?

chain_on(matching_stub.invoke(nil), *chain, &@block)
elsif matching_expectation = find_matching_expectation
chain.shift
matching_expectation.actual_received_count -= 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me why this line is here...can you explain?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectations in the tests were failing due to the expect() setting up an expectation that exactly one call is made. I realised this is because we invoke the first expectation once, and then when the entire chain is called, that first expectation gets called a second time. I guess to me, the least surprising behaviour is that the tests as they are pass. I guess I could increment the expected received count instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get it now. Instead, what do you think about adding a invoke_without_changing_received_count method to MessageExpectation? to me, it feels hacky to manipulate the received counts directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myronmarston that's a really good call. I love how you always come up with this stuff.

@myronmarston
Copy link
Member

I've noticed that if you've set an expectation/allowance before you created a chain, and that expectation/allowance returns nil you get a stubbing method on nil warning. I'm not sure how you feel about that, but I'm pretty sure it's an inevitable consequence.

It's the correct behavior. That said, you could avoid it in your test by doing something like:

allow(obj).to receive(:foo).and_return(double)
allow(obj).to receive_message_chain(:foo, :bar, :bazz)

...because then it would be stubbing the returned double rather than the implicitly returned nil.

This is looking great, @samphippen :).

@mhenrixon
Copy link

Woohoo this is coming along nicely! Good job :) 👍

@fables-tales
Copy link
Member Author

@myronmarston thoughts on matching_expectation.invoke_without_incrementing_received_count and the implementation there in?

@myronmarston
Copy link
Member

LGTM. Let's squash this down and merge it!

BTW, if it's not too much trouble to backport this to 2.99, that would be nice, as I suspect that transpec will be gain the ability to do the conversion and it would be nice to convert to this as folks upgrade. Not essential though.

@fables-tales
Copy link
Member Author

@myronmarston /me wipes sweat of brow. Well that was fun. I'll squash it down to one commit, and then cherry pick it across to 2-99 assuming that's sufficiently easy.

fables-tales pushed a commit that referenced this pull request Nov 18, 2013
Add allow(...).to receive_message_chain
@fables-tales fables-tales merged commit 4662eb0 into master Nov 18, 2013
@fables-tales fables-tales deleted the receive-chained-messages branch November 18, 2013 21:49
@mhenrixon
Copy link

woohooo!! This makes me a really happy camper 👍

@fables-tales
Copy link
Member Author

@myronmarston I had a crack at reconciling this with 2-99 and failed to merge it properly. I'll take another go tomorrow, but this may be unsurprising: these things are now quite out of sync.

@fables-tales
Copy link
Member Author

@myronmarston couldn't merge this into 2-99. I think we'll just leave it for 3.0.0?

@mhenrixon
Copy link

What happened to:

expect(something).to receive_message_chain('whatever.something').with(?)

?

@myronmarston
Copy link
Member

@myronmarston couldn't merge this into 2-99. I think we'll just leave it for 3.0.0?

I'm fine with that.

@mhenrixon -- did stub_chain support with? (Shows you how rarely I've used that feature). What are it's semantics? Is it just constraining the last message?

@mhenrixon
Copy link

Oh sorry about that I thought you guys could read my mind!

No stub_chain didn't support this but stub_chain wasn't an expectation either so to make it truly useful I would expect that .with(args) just constrains the last message. Think @samphippen already suggested this in the originating issue?

@myronmarston
Copy link
Member

No stub_chain didn't support this but stub_chain wasn't an expectation either so to make it truly useful I would expect that .with(args) just constrains the last message. Think @samphippen already suggested this in the originating issue?

The reason we now support expect(...).to receive_message_chain is because the new allow vs expect + a matcher approach made it fall out naturally. We have a lot of other stuff to work on for RSpec 3 and I'm not convinced that adding with is a good idea. receive_message_chain is specifying multiple messages. There are multiple ways to interpret what with means. For example, someone could reasonably think it works like this:

expect(obj).to receive_message_chain("foo.bar.bazz").with(1).with(2).with(3)
obj.foo(1).bar(2).bazz(3)

...or like this:

expect(obj).to receive_message_chain("foo.bar.bazz").with(1)
obj.foo(1).bar(1).bazz(1)

...or like this:

expect(obj).to receive_message_chain("foo.bar.bazz").with(1)
obj.foo(1).bar.bazz

...or like this:

expect(obj).to receive_message_chain("foo.bar.bazz").with(1)
obj.foo.bar.bazz(1)

It's ambiguous (or at least, reasonable to interpret in different ways).

If you care what arguments are received, then use the existing tools available to you.

@mhenrixon
Copy link

Could you guide me to how I could achieve that myself now that all the terribly tough work has been done by you guys already? Could I hack that together myself somehow?

@myronmarston
Copy link
Member

allow(obj).to receive_message_chain(:foo, :bar).and_return(double)
expect(obj.foo.bar).to receive(:bazz).with(/adfads/)

@jsmestad
Copy link

@myronmarston does receive_message_chain still exist?

@myronmarston
Copy link
Member

@myronmarston does receive_message_chain still exist?

Yes. In fact, it's only been in one release: 3.0 (which is also the most recent release). We have no plans to ever remove it.

@stephan-nordnes-eriksen

@myronmarston the receive_message_chain is really nice, but I don't see how useful it is if you can't actually, somehow, test the arguments. Would it not be possible to make the syntax something like this? expect(obj).to receive(:method_name).with("data").receive(:method_two).with("data2").and_return("result")

Example of usecase:

Say I want to test a single-fire object like MessageClass.new("data").send_to("username"). Of course you can use spy on the MessageClass and stub the new method, and then run an expect on the spy. But it is tedious and would be much nicer to have in a chain.

Also, there are probably some piping libraries that could benefit from this as well, think: dataset.filter("whatever").order.first(20).

@myronmarston
Copy link
Member

@stephan-nordnes-eriksen -- please open a new issue. Feature requests posted at the bottom of a merged pull request tend to get lost in the shuffle.

@stephan-nordnes-eriksen

Yea, I agree, but I don't want to create a feature request if it is something that won't be possible to implement.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants