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

Merged
merged 1 commit into from Nov 18, 2013

Conversation

Projects
None yet
6 participants
@samphippen
Member

samphippen commented Nov 13, 2013

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

+ end
+ end
+ end
+end

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

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

@myronmarston

myronmarston Nov 13, 2013

Member

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

This comment has been minimized.

@samphippen

samphippen Nov 13, 2013

Member

framework.rb?

@samphippen

samphippen Nov 13, 2013

Member

framework.rb?

This comment has been minimized.

@JonRowe

JonRowe Nov 14, 2013

Member

Why not leave it in receive.rb?

@JonRowe

JonRowe Nov 14, 2013

Member

Why not leave it in receive.rb?

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

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

@myronmarston

myronmarston Nov 14, 2013

Member

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

+ end
+ end
+ end
+end

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

It looks like this file is a duplicate of receive_message_chain.rb and not even used. Can it be removed?

@myronmarston

myronmarston Nov 13, 2013

Member

It looks like this file is a duplicate of receive_message_chain.rb and not even used. Can it be removed?

This comment has been minimized.

@samphippen

samphippen Nov 13, 2013

Member

Yup, I biffed my git.

@samphippen

samphippen Nov 13, 2013

Member

Yup, I biffed my git.

@@ -0,0 +1,35 @@
+require "forwardable"

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

You're using forwardable below (beyond extending the module), so it looks like you can remove this require and that extension.

@myronmarston

myronmarston Nov 13, 2013

Member

You're using forwardable below (beyond extending the module), so it looks like you can remove this require and that extension.

This comment has been minimized.

@samphippen

samphippen Nov 13, 2013

Member

*not using?

@samphippen

samphippen Nov 13, 2013

Member

*not using?

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

Yeah, "not using" :).

@myronmarston

myronmarston Nov 13, 2013

Member

Yeah, "not using" :).

lib/rspec/mocks/targets.rb
+ Matchers::ReceiveMessages,
+ Matchers::ReceiveMessageChain,
+ ].any? { |m| m === matcher }
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

This would be more idiomatic as matcher_allowed?.

Also, seems simpler to just have a set of allowed matcher classes and then do allowed_matcher_classes.include?(matcher.class).

@myronmarston

myronmarston Nov 13, 2013

Member

This would be more idiomatic as matcher_allowed?.

Also, seems simpler to just have a set of allowed matcher classes and then do allowed_matcher_classes.include?(matcher.class).

+ end
+ end
+ end
+end

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

This is a very isolated unit-testy. While I often favor that approach in testing applications, within RSpec I prefer to do full integration tests of the features as users use them. The normal main downside to full integration tests is that their slow...but they are blazing fast here in RSpec (rspec-mocks' tests take about 500 ms total), and testing this feature entirely via the public API users use has two huge benefits:

  • It gives us much greater confidence the feature works.
  • It makes it much easier to refactor in the future. IMO, ReceiveMessageChain is entirely an implementation detail of this feature. I want to be free to refactor that entirely, while retaining tests that tell me if the feature is still working. Having tests that use ReceiveMessageChain directly inhibits such refactorings.

Just to give you an example of one integration bug that's not (and can't) be caught by these tests: if you look at the implementation of StubChain.stub_chain_on, you'll notice that it relies on Object#stub, which, if the user has disabled the should syntax, will raise a NoMethodError. The tests here are incapable of detecting such an integration error.

@myronmarston

myronmarston Nov 13, 2013

Member

This is a very isolated unit-testy. While I often favor that approach in testing applications, within RSpec I prefer to do full integration tests of the features as users use them. The normal main downside to full integration tests is that their slow...but they are blazing fast here in RSpec (rspec-mocks' tests take about 500 ms total), and testing this feature entirely via the public API users use has two huge benefits:

  • It gives us much greater confidence the feature works.
  • It makes it much easier to refactor in the future. IMO, ReceiveMessageChain is entirely an implementation detail of this feature. I want to be free to refactor that entirely, while retaining tests that tell me if the feature is still working. Having tests that use ReceiveMessageChain directly inhibits such refactorings.

Just to give you an example of one integration bug that's not (and can't) be caught by these tests: if you look at the implementation of StubChain.stub_chain_on, you'll notice that it relies on Object#stub, which, if the user has disabled the should syntax, will raise a NoMethodError. The tests here are incapable of detecting such an integration error.

+ end
+ """
+ When I run `rspec spec/chained_messages.rb`
+ Then the output should contain "1 example, 0 failures"

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

These cukes look like they are in the wrong file. This file is focused on expect, not allow.

@myronmarston

myronmarston Nov 13, 2013

Member

These cukes look like they are in the wrong file. This file is focused on expect, not allow.

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 13, 2013

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(...)
Member

myronmarston commented Nov 13, 2013

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|

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

I think you're missing and_call_original.

@myronmarston

myronmarston Nov 13, 2013

Member

I think you're missing and_call_original.

This comment has been minimized.

@samphippen

samphippen Nov 13, 2013

Member

stub chain does not work with and_call_original:

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

@samphippen

samphippen Nov 13, 2013

Member

stub chain does not work with and_call_original:

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

This comment has been minimized.

@myronmarston

myronmarston Nov 13, 2013

Member

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

myronmarston Nov 13, 2013

Member

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

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 13, 2013

Member

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

Member

myronmarston commented Nov 13, 2013

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

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 14, 2013

Member

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

I've now added these.

Member

samphippen commented Nov 14, 2013

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

This comment has been minimized.

@JonRowe

JonRowe Nov 14, 2013

Member

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

@JonRowe

JonRowe Nov 14, 2013

Member

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

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

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

@myronmarston

myronmarston Nov 14, 2013

Member

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

This comment has been minimized.

@JonRowe

JonRowe Nov 14, 2013

Member

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?

@JonRowe

JonRowe Nov 14, 2013

Member

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?

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

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.

@myronmarston

myronmarston Nov 14, 2013

Member

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

This comment has been minimized.

Show comment
Hide comment
@JonRowe

JonRowe Nov 14, 2013

Member

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

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?

lib/rspec/mocks/targets.rb
+ Matchers::ReceiveMessages,
+ Matchers::ReceiveMessageChain,
+ ]
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

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.

@myronmarston

myronmarston Nov 14, 2013

Member

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.

+ allow(object).to receive_message_chain(:to_a, :length) { 3 }
+
+ expect(object.to_a.length).to eq(3)
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

It would be good to have a spec that shows that { } takes precedence over do...end when both block forms are given (since the curly-brace block binds directly to receive_message_chain but do...end binds to to).

@myronmarston

myronmarston Nov 14, 2013

Member

It would be good to have a spec that shows that { } takes precedence over do...end when both block forms are given (since the curly-brace block binds directly to receive_message_chain but do...end binds to to).

lib/rspec/mocks/error_generator.rb
@@ -118,8 +118,8 @@ def raise_wrong_arity_error(args_to_yield, arity)
# @private
def raise_only_valid_on_a_partial_mock(method)

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

Can you change the method name to ..._partial_double as well?

@myronmarston

myronmarston Nov 14, 2013

Member

Can you change the method name to ..._partial_double as well?

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

good spot! (regex replacement fail :))

@samphippen

samphippen Nov 14, 2013

Member

good spot! (regex replacement fail :))

lib/rspec/mocks/stub_chain.rb
@@ -20,11 +20,11 @@ def stub_chain
matching_stub.invoke(nil).stub_chain(*chain, &block)

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

The call to stub_chain here will fail if used with the :should syntax disabled. Would be good to start with a failing test, then fix it to not call that anymore. Reading the code, it looks like this line will get hit with a spec like this:

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

myronmarston Nov 14, 2013

Member

The call to stub_chain here will fail if used with the :should syntax disabled. Would be good to start with a failing test, then fix it to not call that anymore. Reading the code, it looks like this line will get hit with a spec like this:

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

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

@myronmarston So all the specs now run with only the expect syntax and they're passing without changing this. 😕

@samphippen

samphippen Nov 14, 2013

Member

@myronmarston So all the specs now run with only the expect syntax and they're passing without changing this. 😕

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

Have you tried adding an example like I outlined there? Alternately, if you want to figure out the code path that needs that, try changing that line to raise "boom" and see what fails -- based on that, you can then write an example using the new syntax that hits this line.

@myronmarston

myronmarston Nov 14, 2013

Member

Have you tried adding an example like I outlined there? Alternately, if you want to figure out the code path that needs that, try changing that line to raise "boom" and see what fails -- based on that, you can then write an example using the new syntax that hits this line.

+ end
+
+ context "when only the expect syntax is enabled" do
+ include_context "with syntax", :expect

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

I think I'd prefer to see the :should syntax disabled for all of the specs in this file. I can't think of a way that having the :should syntax enabled would break it, but there are many branches and code paths to the implementation of RSpec::Mocks::StubChain and it would give me greater confidence that it works for all of them if :should is disabled for all the specs in this file. Or, if you do want to test that it works with :should enabled, that's fine -- just flip it so that there's only one test where it is enabled and all the rest where is is disabled.

@myronmarston

myronmarston Nov 14, 2013

Member

I think I'd prefer to see the :should syntax disabled for all of the specs in this file. I can't think of a way that having the :should syntax enabled would break it, but there are many branches and code paths to the implementation of RSpec::Mocks::StubChain and it would give me greater confidence that it works for all of them if :should is disabled for all the specs in this file. Or, if you do want to test that it works with :should enabled, that's fine -- just flip it so that there's only one test where it is enabled and all the rest where is is disabled.

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

👍

+ end
+ end
+ end
+end

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

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

myronmarston Nov 14, 2013

Member

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.

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

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

@samphippen

samphippen Nov 14, 2013

Member

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

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

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.

@myronmarston

myronmarston Nov 14, 2013

Member

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.

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

I will add a spec for this later :)

@samphippen

samphippen Nov 14, 2013

Member

I will add a spec for this later :)

This comment has been minimized.

@samphippen

samphippen Nov 15, 2013

Member

done

+module RSpec
+ module Mocks
+ module Matchers
+ class ReceiveMessageChain

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

A big part of being SemVer compliant is being explicit about what is part of the public API and what is not. Every class should have yard comments documenting that. I would make this @api private since it's not intended to be instantiated directly by end users.

@myronmarston

myronmarston Nov 14, 2013

Member

A big part of being SemVer compliant is being explicit about what is part of the public API and what is not. Every class should have yard comments documenting that. I would make this @api private since it's not intended to be instantiated directly by end users.

@@ -124,6 +124,10 @@ def receive_messages(message_return_value_hash)
matcher
end
+ def receive_message_chain(*messages, &block)

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

This method needs yard docs added for it. See the big block of comments at the bottom of the file.

@myronmarston

myronmarston Nov 14, 2013

Member

This method needs yard docs added for it. See the big block of comments at the bottom of the file.

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

Now added.

@samphippen

samphippen Nov 14, 2013

Member

Now added.

+
+ private
+
+ attr_reader :stubber

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

Now that you are not injecting a test double for the stubber in your tests here I think it makes less sense to support the stubber being injectable. Thoughts on removing this reader and the stubber arg from initialize, opting to just directly reference StubChain instead?

@myronmarston

myronmarston Nov 14, 2013

Member

Now that you are not injecting a test double for the stubber in your tests here I think it makes less sense to support the stubber being injectable. Thoughts on removing this reader and the stubber arg from initialize, opting to just directly reference StubChain instead?

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

I'm sort of on the fence about this one. I really like DI if it's usable. I do, however, foresee this class being very coupled to stubchain (or at least it's interface) by nature of what it does.

I think directly referencing StubChain here is fine though.

@samphippen

samphippen Nov 14, 2013

Member

I'm sort of on the fence about this one. I really like DI if it's usable. I do, however, foresee this class being very coupled to stubchain (or at least it's interface) by nature of what it does.

I think directly referencing StubChain here is fine though.

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

I like DI a lot, too, but my general rule of thumb is to only add it if I use it in one place. It's not being used anywhere here, so it feels like overhead to me.

@myronmarston

myronmarston Nov 14, 2013

Member

I like DI a lot, too, but my general rule of thumb is to only add it if I use it in one place. It's not being used anywhere here, so it feels like overhead to me.

lib/rspec/mocks/targets.rb
+ when Matchers::ReceiveMessages
+ raise_negation_unsupported(method_name, matcher)
+ when Matchers::ReceiveMessageChain
+ raise_negation_unsupported(method_name, matcher)

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

These last two when clauses can be combined into one:

when Matchers::ReceiveMessages, Matchers::ReceiveMessageChain
  raise_negation_unsupported(method_name, matcher)
@myronmarston

myronmarston Nov 14, 2013

Member

These last two when clauses can be combined into one:

when Matchers::ReceiveMessages, Matchers::ReceiveMessageChain
  raise_negation_unsupported(method_name, matcher)

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

👍

+
+
+module RSpec::Mocks::Matchers
+ describe "allow(...).to receive_message_chain(...)" do

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

This example group describes more than just allow(...).to receive_message_chain. Should this just be describe "receive_message_chain" do?

@myronmarston

myronmarston Nov 14, 2013

Member

This example group describes more than just allow(...).to receive_message_chain. Should this just be describe "receive_message_chain" do?

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

👍

+ expect {
+ expect_any_instance_of(Object).to receive_message_chain(:to_a, :length => 3)
+ }.to raise_error(/does not work with `expect_any_instance_of`/)
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

What is the issue with expect_any_instance_of? IMO, it seems inconsistent that allow, expect and allow_any_instance_of all support this but expect_any_instance_of doesn't. It would be more consistent to get it to work or to disallow it with expect as well.

@myronmarston

myronmarston Nov 14, 2013

Member

What is the issue with expect_any_instance_of? IMO, it seems inconsistent that allow, expect and allow_any_instance_of all support this but expect_any_instance_of doesn't. It would be more consistent to get it to work or to disallow it with expect as well.

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

I worked it out on a second look. Something about that code was confusing, but I think I can add it now :)

🍺

@samphippen

samphippen Nov 14, 2013

Member

I worked it out on a second look. Something about that code was confusing, but I think I can add it now :)

🍺

+ expect(obj.a).to eq 1
+ expect(obj.b).to eq 2
+ end
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member
  • What prompted adding this?
  • The example group this is in is allow(...).to receive_messages(:a => 1, :b => 2) -- but this is expect_any_instance_of.
@myronmarston

myronmarston Nov 14, 2013

Member
  • What prompted adding this?
  • The example group this is in is allow(...).to receive_messages(:a => 1, :b => 2) -- but this is expect_any_instance_of.

This comment has been minimized.

@samphippen

samphippen Nov 14, 2013

Member

just a git fail plus getting a little confused whilst looking at another matcher and testing an assumption.

@samphippen

samphippen Nov 14, 2013

Member

just a git fail plus getting a little confused whilst looking at another matcher and testing an assumption.

lib/rspec/mocks/syntax.rb
+ # indicate real problems (think fluent interfaces), `stub_chain` still
+ # results in brittle examples. For example, if you write
+ # `foo.stub_chain(:bar, :baz => 37)` in a spec and then the
+ # implementation calls `foo.baz.bar`, the stub will not work.

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

This description mentions stub_chain but should mention receive_message_chain instead.

@myronmarston

myronmarston Nov 14, 2013

Member

This description mentions stub_chain but should mention receive_message_chain instead.

lib/rspec/mocks/syntax.rb
+ # double.foo.bar # => :baz
+ #
+ # # Common use in Rails/ActiveRecord:
+ # Article.stub_chain("recent.published") { [Article.new] }

This comment has been minimized.

@myronmarston

myronmarston Nov 14, 2013

Member

Ditto here: this shouldn't mention stub_chain.

@myronmarston

myronmarston Nov 14, 2013

Member

Ditto here: this shouldn't mention stub_chain.

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 15, 2013

Member

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

Member

samphippen commented Nov 15, 2013

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

samphippen added a commit that referenced this pull request Nov 15, 2013

+
+ def create_message_expectation_on(instance)
+ ::RSpec::Mocks::StubChain.expect_chain_on(instance, *@expectation_args, &@expectation_block)
+ @expectation_fulfilled = true

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

Intuitively, this looks wrong...at this point, how do we know if the expectation is fulfilled?

@myronmarston

myronmarston Nov 16, 2013

Member

Intuitively, this looks wrong...at this point, how do we know if the expectation is fulfilled?

This comment has been minimized.

@samphippen

samphippen Nov 16, 2013

Member

I did this so that this expectation does not fail, instead relying on the chain to raise an error if the expectation is not met. Thinking about it, it probably makes sense to put this in an override of playback! after super has returned, such that we're fulfilled if playback! returns (i.e. does not throw a mocking related error). WDYT?

@samphippen

samphippen Nov 16, 2013

Member

I did this so that this expectation does not fail, instead relying on the chain to raise an error if the expectation is not met. Thinking about it, it probably makes sense to put this in an override of playback! after super has returned, such that we're fulfilled if playback! returns (i.e. does not throw a mocking related error). WDYT?

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

On the surface that sounds better, but it's hard to say until I see it. If it's simplest to keep this here, that's fine; just put a comment explaining why.

@myronmarston

myronmarston Nov 16, 2013

Member

On the surface that sounds better, but it's hard to say until I see it. If it's simplest to keep this here, that's fine; just put a comment explaining why.

+ observe!(method_name)
+ message_chains.add(method_name, ExpectChainChain.new(self, *args, &block))
+ end
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

I'd prefer not to add this. IMO, one win of the new syntax is that things compose nicely so that expecting a chain falls out of it naturally. I want to encourage folks to upgrade, so I don't see a reason to backport the feature to the old syntax.

@myronmarston

myronmarston Nov 16, 2013

Member

I'd prefer not to add this. IMO, one win of the new syntax is that things compose nicely so that expecting a chain falls out of it naturally. I want to encourage folks to upgrade, so I don't see a reason to backport the feature to the old syntax.

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

Nevermind: I see now that this is needed for your implementation. I originally thought that you had added it so that folks could do SomeClass.any_instance.expect_chain, which I'm not very interested in supporting.

This needs some kind of YARD docs. I'd prefer to have it labeled @api private so that it's officially labeled as a non-public API.

@myronmarston

myronmarston Nov 16, 2013

Member

Nevermind: I see now that this is needed for your implementation. I originally thought that you had added it so that folks could do SomeClass.any_instance.expect_chain, which I'm not very interested in supporting.

This needs some kind of YARD docs. I'd prefer to have it labeled @api private so that it's officially labeled as a non-public API.

This comment has been minimized.

@samphippen

samphippen Nov 16, 2013

Member

Now labelled as api private.

@samphippen

samphippen Nov 16, 2013

Member

Now labelled as api private.

lib/rspec/mocks/error_generator.rb
- __raise "#{intro} is a pure mock object. `#{method}` is only " +
- "available on a partial mock object."
+ def raise_only_valid_on_a_partial_double(method)
+ __raise "#{intro} is a pure double object. `#{method}` is only " +

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

Maybe pure test double is better than pure double object?

@myronmarston

myronmarston Nov 16, 2013

Member

Maybe pure test double is better than pure double object?

+
+ def negative_failure_message
+ raise NegationUnsupportedError.new("`expect(...).not_to receive_message_chain` is not supported since it doesn't really make sense. What would it even mean?")
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

I'd prefer to see this logic defined in does_not_match? as that's what rspec-expectations will initially call, and we want to fail early.

Also, I'm not a fan of super long lines like this; I'd prefer it see it formatted nicely like this:

def setup_negative_expectation(subject)
raise NegationUnsupportedError,
"`expect(...).to_not receive_messages` is not supported since it " +
"doesn't really make sense. What would it even mean?"
end
alias does_not_match? setup_negative_expectation

@myronmarston

myronmarston Nov 16, 2013

Member

I'd prefer to see this logic defined in does_not_match? as that's what rspec-expectations will initially call, and we want to fail early.

Also, I'm not a fan of super long lines like this; I'd prefer it see it formatted nicely like this:

def setup_negative_expectation(subject)
raise NegationUnsupportedError,
"`expect(...).to_not receive_messages` is not supported since it " +
"doesn't really make sense. What would it even mean?"
end
alias does_not_match? setup_negative_expectation

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

Actually, I like the setup_negative_expectation; alias does_not_match? setup_negative_expectation pattern there -- might as well follow that here.

@myronmarston

myronmarston Nov 16, 2013

Member

Actually, I like the setup_negative_expectation; alias does_not_match? setup_negative_expectation pattern there -- might as well follow that here.

lib/rspec/mocks/stub_chain.rb
+ elsif expect_or_allow == :allow
+ ::RSpec::Mocks.allow_message(object, message, {}) { next_in_chain }
+ end
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

You have expect_or_allow conditionals in 3 places. I think we'd be better off with two separate classes: one for expect, one for allow, and then there would be no need for conditionals.

@myronmarston

myronmarston Nov 16, 2013

Member

You have expect_or_allow conditionals in 3 places. I think we'd be better off with two separate classes: one for expect, one for allow, and then there would be no need for conditionals.

+ expect {
+ expect(object).to receive_message_chain(:to_a, :farce, :length => 3)
+ object.to_a
+ RSpec::Mocks.space.verify_all

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

This can just be verify_all (there's a spec helper method for this).

@myronmarston

myronmarston Nov 16, 2013

Member

This can just be verify_all (there's a spec helper method for this).

+ expect {
+ expect(object).to receive_message_chain(:to_a, :length => 3)
+ object.to_a.length
+ RSpec::Mocks.space.verify_all

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

This can just be verify_all (there's a spec helper method for this).

@myronmarston

myronmarston Nov 16, 2013

Member

This can just be verify_all (there's a spec helper method for this).

+
+ expect {
+ expect_any_instance_of(Object).to receive_message_chain(:to_a, :length => 3)
+ RSpec::Mocks.space.verify_all

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

This can just be verify_all (there's a spec helper method for this).

@myronmarston

myronmarston Nov 16, 2013

Member

This can just be verify_all (there's a spec helper method for this).

+ expect(o.to_a.length).to eq(3)
+ end
+
+ it "does not work with expect_any_instance_of" do

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

This example makes it look like it does work with expect_any_instance_of even though it says it didn't.

@myronmarston

myronmarston Nov 16, 2013

Member

This example makes it look like it does work with expect_any_instance_of even though it says it didn't.

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

Also, would be nice to show an example where the entire chain is called and no error is rasied. You have one for expect but not for expect_any_instance_of.

@myronmarston

myronmarston Nov 16, 2013

Member

Also, would be nice to show an example where the entire chain is called and no error is rasied. You have one for expect but not for expect_any_instance_of.

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 16, 2013

Member

This is looking really fantastic @samphippen -- almost ready to merge. Left a couple more comments. (The big one is the suggestion to refactor StubChain to leverage polymorphism rather than a bunch of conditionals). Also, it would be great if you could squash this before we merge. Thanks!

Member

myronmarston commented Nov 16, 2013

This is looking really fantastic @samphippen -- almost ready to merge. Left a couple more comments. (The big one is the suggestion to refactor StubChain to leverage polymorphism rather than a bunch of conditionals). Also, it would be great if you could squash this before we merge. Thanks!

Changelog.md
+ `expect(...).to receive_message_chain(...)` is supported, making it possible
+ to create message expectations, as well stubs, using this matcher.
+ (Sam Phippen)
+

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

It's a little odd that the primary usage of this (e.g. allow(...).to receive_message_chain(...)) isn't highlighted here, and instead the expect form is. Also, calling receive_message_chain a "matcher" is stretching it (even though it implements the rspec-expectations matcher protocol). I'd probably phrase this something like:

  • Add receive_message_chain which provides the functionality of the old stub_chain for the new allow/expect syntax. Use it like so: allow(...).to receive_message_chain(:foo, :bar, :bazz). (Sam Phippen).
@myronmarston

myronmarston Nov 16, 2013

Member

It's a little odd that the primary usage of this (e.g. allow(...).to receive_message_chain(...)) isn't highlighted here, and instead the expect form is. Also, calling receive_message_chain a "matcher" is stretching it (even though it implements the rspec-expectations matcher protocol). I'd probably phrase this something like:

  • Add receive_message_chain which provides the functionality of the old stub_chain for the new allow/expect syntax. Use it like so: allow(...).to receive_message_chain(:foo, :bar, :bazz). (Sam Phippen).

samphippen added a commit that referenced this pull request Nov 16, 2013

lib/rspec/mocks/stub_chain.rb
+ def chain_on(next_in_chain, *chain, &block)
+ StubChain.expect_chain_on(next_in_chain, *chain, &block)
+ end
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

This implementation is better in terms of polymorphism (nice work!) but it creates an excessive number of objects -- which in turn need to be GC'd. Consider an expression like receive_message_chain(:foo, :bar, :bazz). Here's the objects this will create:

  • stub_chain_on is called, which creates an instance of StubChain and calls stub_chain on it.
  • In stub_chain, it creates an instance of AllowChainStrategy and calls walk_chain.
  • That in turn calls AllowChainStrategy#chain_on, which turns around and calls stub_chain_on, which creates another instance of StubChain and repeats the process with one less message in the chain.

There's also the test doubles that get created (but those have to be created).

The number of throw-away objects involved in this process scales linearly with the number of messages in the chain.

I think there's a better way to do this that will involve much fewer object instantiations (which should perform better) and be easier to maintain, to boot:

  • Split this up in to two classes: StubChain and ExpectChain.
  • Have the recursion recurse onto itself rather than creating a new instance to recurse onto.
  • With this setup, there won't be a need for separate strategy classes--StubChain and ExpectChain will basically implement those strategies within themselves.
  • They can share a base class if there's any common logic. (Basically, this would be an implementation of the template method pattern if you're familiar with that term).
  • If you like, the main entry point method can be named the same (something like setup_chain_on), and from the receive_message_chain matcher it can either call StubChain.setup_chain_on(...) or ExpectChain.setup_chain_on(...) appropriately.

Let me know if that makes sense.

@myronmarston

myronmarston Nov 16, 2013

Member

This implementation is better in terms of polymorphism (nice work!) but it creates an excessive number of objects -- which in turn need to be GC'd. Consider an expression like receive_message_chain(:foo, :bar, :bazz). Here's the objects this will create:

  • stub_chain_on is called, which creates an instance of StubChain and calls stub_chain on it.
  • In stub_chain, it creates an instance of AllowChainStrategy and calls walk_chain.
  • That in turn calls AllowChainStrategy#chain_on, which turns around and calls stub_chain_on, which creates another instance of StubChain and repeats the process with one less message in the chain.

There's also the test doubles that get created (but those have to be created).

The number of throw-away objects involved in this process scales linearly with the number of messages in the chain.

I think there's a better way to do this that will involve much fewer object instantiations (which should perform better) and be easier to maintain, to boot:

  • Split this up in to two classes: StubChain and ExpectChain.
  • Have the recursion recurse onto itself rather than creating a new instance to recurse onto.
  • With this setup, there won't be a need for separate strategy classes--StubChain and ExpectChain will basically implement those strategies within themselves.
  • They can share a base class if there's any common logic. (Basically, this would be an implementation of the template method pattern if you're familiar with that term).
  • If you like, the main entry point method can be named the same (something like setup_chain_on), and from the receive_message_chain matcher it can either call StubChain.setup_chain_on(...) or ExpectChain.setup_chain_on(...) appropriately.

Let me know if that makes sense.

This comment has been minimized.

@samphippen

samphippen Nov 16, 2013

Member

@myronmarston I think passing in a strategy in the constructor of StubChain would achieve this aim. That way we don't need to construct a new StubChain each time, we can just call walk_chain to achieve recursion. I really like the DI of the strategy here so that the StubChain object becomes configurable by introducing new collaborators.

@samphippen

samphippen Nov 16, 2013

Member

@myronmarston I think passing in a strategy in the constructor of StubChain would achieve this aim. That way we don't need to construct a new StubChain each time, we can just call walk_chain to achieve recursion. I really like the DI of the strategy here so that the StubChain object becomes configurable by introducing new collaborators.

This comment has been minimized.

@samphippen

samphippen Nov 16, 2013

Member

@myronmarston WDYT/would you still like two classes?

@samphippen

samphippen Nov 16, 2013

Member

@myronmarston WDYT/would you still like two classes?

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

One of the reasons I favor two classes (w/o the separate strategy) is because this class currently has two entirely separate sets of APIs:

  • .stub_chain_on/#stub_chain
  • .expect_chain_on/#expect_chain

You either do use one set or the other, never both. So it feels very odd to me to keep them on the same object given that these uses are disjoint. To me, it would feel more natural to have two separate classes that implement the same interface (and potentially use a base class for implementing the common bits).

@myronmarston

myronmarston Nov 16, 2013

Member

One of the reasons I favor two classes (w/o the separate strategy) is because this class currently has two entirely separate sets of APIs:

  • .stub_chain_on/#stub_chain
  • .expect_chain_on/#expect_chain

You either do use one set or the other, never both. So it feels very odd to me to keep them on the same object given that these uses are disjoint. To me, it would feel more natural to have two separate classes that implement the same interface (and potentially use a base class for implementing the common bits).

+ o.to_a.length
+ end
+
+ it "gives the { } block prescedence over the do block" do

This comment has been minimized.

@myronmarston

myronmarston Nov 16, 2013

Member

Can this spec be moved directly under the { } block one and the do...end block one? It seems very related, so it's odd to have it orphaned down here.

@myronmarston

myronmarston Nov 16, 2013

Member

Can this spec be moved directly under the { } block one and the do...end block one? It seems very related, so it's odd to have it orphaned down here.

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 16, 2013

Member

This is very close now :). Left a largeish comment on the implementation of StubChain but that's basically it.

Member

myronmarston commented Nov 16, 2013

This is very close now :). Left a largeish comment on the implementation of StubChain but that's basically it.

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 16, 2013

Member

@myronmarston thoughts on the new chain classes?

Member

samphippen commented Nov 16, 2013

@myronmarston thoughts on the new chain classes?

+
+ def expectation(object, message, returned_object)
+ raise NotImplementedError.new
+ end

This comment has been minimized.

@myronmarston

myronmarston Nov 17, 2013

Member

I generally like this pattern when end users are meant to subclass (as the definition with NotImplementedError makes it clear that they are to define it in their subclass), but in this case I'm in favor of not having this, for a few reasons:

  • This is only meant to be subclassed by us, and we've implemented the override in both cases.
  • This code will never be executed.
  • Every method def adds to the amount of memory the ruby process uses (and the amount of time it takes to boot) -- so while this one method def probably won't be noticable, I prefer not to add method defs that will never be called, as they can add up over time.

What do you think about removing this?

@myronmarston

myronmarston Nov 17, 2013

Member

I generally like this pattern when end users are meant to subclass (as the definition with NotImplementedError makes it clear that they are to define it in their subclass), but in this case I'm in favor of not having this, for a few reasons:

  • This is only meant to be subclassed by us, and we've implemented the override in both cases.
  • This code will never be executed.
  • Every method def adds to the amount of memory the ruby process uses (and the amount of time it takes to boot) -- so while this one method def probably won't be noticable, I prefer not to add method defs that will never be called, as they can add up over time.

What do you think about removing this?

This comment has been minimized.

@samphippen

samphippen Nov 18, 2013

Member

👍

+
+ def find_matching_stub
+ ::RSpec::Mocks.proxy_for(object).
+ __send__(:find_matching_method_stub, chain.first.to_sym)

This comment has been minimized.

@myronmarston

myronmarston Nov 17, 2013

Member

Sorry I didn't notice this before...but it looks like this needs to be defined in the subclasses as well: there's find_matching_method_stub and find_matching_expectation. Before fixing this, it'd be good to add a failing test first. Reading the code, I think it would come into play in a case like this:

expect(obj).to receive(:foo)
expect(obj).to receive_message_chain(:foo, :bar, :bazz)

I'm not 100% sure on that, though.

@myronmarston

myronmarston Nov 17, 2013

Member

Sorry I didn't notice this before...but it looks like this needs to be defined in the subclasses as well: there's find_matching_method_stub and find_matching_expectation. Before fixing this, it'd be good to add a failing test first. Reading the code, I think it would come into play in a case like this:

expect(obj).to receive(:foo)
expect(obj).to receive_message_chain(:foo, :bar, :bazz)

I'm not 100% sure on that, though.

This comment has been minimized.

@samphippen

samphippen Nov 17, 2013

Member

I'll take a look.

@samphippen

samphippen Nov 17, 2013

Member

I'll take a look.

lib/rspec/mocks/expect_chain.rb
+ end
+ end
+ end
+end

This comment has been minimized.

@myronmarston

myronmarston Nov 17, 2013

Member

Given how small this class is, do you think it warrants its own file? I tend to prefer the one-concept-per-file organization rather than one-class-per-file (particularly since 1.9 has known require perf issues). What do you think about combining this class, StubChain and MessageChain into the same file?

@myronmarston

myronmarston Nov 17, 2013

Member

Given how small this class is, do you think it warrants its own file? I tend to prefer the one-concept-per-file organization rather than one-class-per-file (particularly since 1.9 has known require perf issues). What do you think about combining this class, StubChain and MessageChain into the same file?

This comment has been minimized.

@samphippen

samphippen Nov 17, 2013

Member

I'll move the various chains into one file.

@samphippen

samphippen Nov 17, 2013

Member

I'll move the various chains into one file.

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 17, 2013

Member

@myronmarston thoughts on the new chain classes?

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

Member

myronmarston commented Nov 17, 2013

@myronmarston thoughts on the new chain classes?

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

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 17, 2013

Member

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

Member

samphippen commented Nov 17, 2013

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

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 17, 2013

Member

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

Member

samphippen commented Nov 17, 2013

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

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 17, 2013

Member

@myronmarston can you take a look at this please?

Member

samphippen commented Nov 17, 2013

@myronmarston can you take a look at this please?

lib/rspec/mocks/message_chain.rb
+ chain_on(matching_stub.invoke(nil), *chain, &@block)
+ elsif matching_expectation = find_matching_expectation
+ chain.shift
+ matching_expectation.actual_received_count -= 1

This comment has been minimized.

@myronmarston

myronmarston Nov 18, 2013

Member

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

@myronmarston

myronmarston Nov 18, 2013

Member

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

This comment has been minimized.

@samphippen

samphippen Nov 18, 2013

Member

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?

@samphippen

samphippen Nov 18, 2013

Member

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?

This comment has been minimized.

@myronmarston

myronmarston Nov 18, 2013

Member

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.

@myronmarston

myronmarston Nov 18, 2013

Member

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.

This comment has been minimized.

@samphippen

samphippen Nov 18, 2013

Member

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

@samphippen

samphippen Nov 18, 2013

Member

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

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 18, 2013

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

Member

myronmarston commented Nov 18, 2013

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

This comment has been minimized.

Show comment
Hide comment
@mhenrixon

mhenrixon Nov 18, 2013

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

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

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 18, 2013

Member

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

Member

samphippen commented Nov 18, 2013

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

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 18, 2013

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.

Member

myronmarston commented Nov 18, 2013

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.

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 18, 2013

Member

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

Member

samphippen commented Nov 18, 2013

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

samphippen added a commit that referenced this pull request Nov 18, 2013

Merge pull request #467 from rspec/receive-chained-messages
Add allow(...).to receive_message_chain

@samphippen samphippen merged commit 4662eb0 into master Nov 18, 2013

1 check passed

default The Travis CI build passed
Details

@samphippen samphippen deleted the receive-chained-messages branch Nov 18, 2013

@mhenrixon

This comment has been minimized.

Show comment
Hide comment
@mhenrixon

mhenrixon Nov 18, 2013

woohooo!! This makes me a really happy camper 👍

woohooo!! This makes me a really happy camper 👍

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 18, 2013

Member

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

Member

samphippen commented Nov 18, 2013

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

@yujinakayama yujinakayama referenced this pull request in yujinakayama/transpec Nov 20, 2013

Closed

Support conversion of #stub_chain #21

@samphippen

This comment has been minimized.

Show comment
Hide comment
@samphippen

samphippen Nov 21, 2013

Member

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

Member

samphippen commented Nov 21, 2013

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

@mhenrixon

This comment has been minimized.

Show comment
Hide comment
@mhenrixon

mhenrixon Nov 21, 2013

What happened to:

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

?

What happened to:

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

?

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 21, 2013

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?

Member

myronmarston commented Nov 21, 2013

@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

This comment has been minimized.

Show comment
Hide comment
@mhenrixon

mhenrixon Nov 21, 2013

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?

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

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 21, 2013

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.

Member

myronmarston commented Nov 21, 2013

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

This comment has been minimized.

Show comment
Hide comment
@mhenrixon

mhenrixon Nov 21, 2013

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?

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

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Nov 21, 2013

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

myronmarston commented Nov 21, 2013

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

This comment has been minimized.

Show comment
Hide comment
@jsmestad

jsmestad Jul 17, 2014

@myronmarston does receive_message_chain still exist?

@myronmarston does receive_message_chain still exist?

@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Jul 17, 2014

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.

Member

myronmarston commented Jul 17, 2014

@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

This comment has been minimized.

Show comment
Hide comment
@stephan-nordnes-eriksen

stephan-nordnes-eriksen Feb 18, 2015

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

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Feb 18, 2015

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.

Member

myronmarston commented Feb 18, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@stephan-nordnes-eriksen

stephan-nordnes-eriksen Feb 18, 2015

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

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 join this conversation on GitHub. Already have an account? Sign in to comment