Allow multiple message allowances/expectations via `receive_messages` #399

Merged
merged 28 commits into from Sep 12, 2013

Conversation

Projects
None yet
5 participants
Owner

JonRowe commented Aug 9, 2013

Further to #368 this allow multiple message allowances/expectations via receive_messages.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

lib/rspec/mocks/matchers/receives_messages.rb
+ @receivers = message_value_hash.map do |method_name, value|
+ Matchers::Receive.new(method_name, proc { value })
+ end
+ end
+
+ %w[
+ setup_expecation matches? setup_negative_expectation does_not_match?
+ setup_allowance setup_any_instance_expectation setup_any_instance_expectation
+ setup_any_instance_negative_expectation setup_any_instance_allowance
+ ].each do |method_name|
+ define_method(method_name) do |subject, &block|
+ @receivers.each do |receiver|
+ receiver.send(method_name,subject,&block)
+ end
+ end
+ end
@myronmarston

myronmarston Aug 9, 2013

Owner

It seems like a lot of overhead to go through to repeatedly delegate to the Receive matcher here (which in turn delegates to a more fundamental underlying mechanism). Among other things, if you have provided a hash of 6 messages, the receive matchers will each get the mock proxy individually, causing 6 mock proxy lookups when one would suffice.

What would it look like to bypass the receive matcher?

@JonRowe

JonRowe Aug 10, 2013

Owner

It's the same as setting up each expectation / allowance, so it's no additional overhead, but it looked to me like it would involve a fair amount of repetition to extract this out...

@myronmarston

myronmarston Aug 10, 2013

Owner

Actually, in #401 @xaviershay has some perf improvements that this PR should take advantage of, I think: he's introduced the concept of a "simple stub" for these sorts of message/return-value pairs, and it looks to greatly improve perf.

So, I'm thinking we should merge his PR first (it sounds like he'll address my feedback soon and it's very close to being ready to merge) and then refactor this to use add_simple_stub under the covers.

Thoughts?

@JonRowe

JonRowe Aug 10, 2013

Owner

I like that idea, I'll take a look when it's merged.

@myronmarston myronmarston commented on an outdated diff Aug 9, 2013

lib/rspec/mocks/matchers/receives_messages.rb
+ class ReceivesMessages
+
+ def initialize(message_value_hash)
+ @receivers = message_value_hash.map do |method_name, value|
+ Matchers::Receive.new(method_name, proc { value })
+ end
+ end
+
+ %w[
+ setup_expecation matches? setup_negative_expectation does_not_match?
+ setup_allowance setup_any_instance_expectation setup_any_instance_expectation
+ setup_any_instance_negative_expectation setup_any_instance_allowance
+ ].each do |method_name|
+ define_method(method_name) do |subject, &block|
+ @receivers.each do |receiver|
+ receiver.send(method_name,subject,&block)
@myronmarston

myronmarston Aug 9, 2013

Owner

If we do stick with this implementation, please put spaces, between, your, arguments.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

lib/rspec/mocks/targets.rb
@@ -11,7 +11,7 @@ def initialize(target)
def self.delegate_to(matcher_method, options = {})
method_name = options.fetch(:from) { :to }
define_method(method_name) do |matcher, &block|
- unless Matchers::Receive === matcher
+ unless Matchers::Receive === matcher || Matchers::ReceivesMessages === matcher
raise UnsupportedMatcherError, "only the `receive` matcher is supported " +
@myronmarston

myronmarston Aug 9, 2013

Owner

The message should probably change to mention receive and receive_messages.

@JonRowe

JonRowe Aug 10, 2013

Owner

Updated the message.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+module RSpec
+ module Mocks
+ describe "allow(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:obj) { double "Object" }
@myronmarston

myronmarston Aug 9, 2013

Owner

Why bother with the "Object" argument here?

@JonRowe

JonRowe Aug 10, 2013

Owner

I like having a name, I can drop it if needs be.

@myronmarston

myronmarston Aug 13, 2013

Owner

I think having a name is useful when your test double is playing a role. The role it's playing here is "generic test double"...which really isn't a named role.

Not a big deal either way, though, I was just curious :).

@JonRowe

JonRowe Aug 13, 2013

Owner

TBH It's mostly force of habit from having multiple doubles, which when anonymous makes it hard to see which is which.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+
+ it "allows the object to respond to multiple messages" do
+ allow(obj).to receive_messages(:a => 1, :b => 2)
+ expect(obj.a).to eq 1
+ expect(obj.b).to eq 2
+ end
+
+ it "allows single expectations" do
+ allow(obj).to receive_messages(:a => 1)
+ expect(obj.a).to eq 1
+ end
+
+ it "complains if a block is given" do
+ expect do
+ allow(obj).to receive_messages(:a => 1) { "implementation" }
+ end.to raise_error "Implementation blocks arn't supported with `receive_messages`"
@myronmarston

myronmarston Aug 9, 2013

Owner

This isn't a big deal at all (and I'm sure we have other places that do what you've done here), but I prefer curly braces for block expectations over do...end because the do/end words obscure the English phrasing. Thoughts on using curlies instead?

@JonRowe

JonRowe Aug 10, 2013

Owner

I prefer do...end for multi-line blocks... that's all, I'm ok with changing these...

@myronmarston

myronmarston Aug 10, 2013

Owner

I generally do, too...but I make an exception when the expression is meant to read like an English phrase, since "do" and "end" are english words. That's just my preference, though. And as I said, it's not a big deal, and certainly not a merge blocker.

@JonRowe

JonRowe Aug 10, 2013

Owner

I switched em anyway.

@myronmarston myronmarston commented on an outdated diff Aug 9, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+ expect(obj.a).to eq 1
+ end
+
+ it "complains if a block is given" do
+ expect do
+ allow(obj).to receive_messages(:a => 1) { "implementation" }
+ end.to raise_error "Implementation blocks arn't supported with `receive_messages`"
+ end
+ end
+
+ describe "expect(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:reporter) { RSpec::Core::Reporter.new }
+
+ it "sets up multiple expectations" do
+ expect(reporter).to receive(:example_passed).with("will pass")
+ expect(reporter).to receive(:example_failed).with("will fail")
@myronmarston

myronmarston Aug 9, 2013

Owner

Mocking the reporter seems kinda like an odd and potentially brittle way to demonstrate pass/fail...

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+
+ example_group = ::RSpec::Core::ExampleGroup.describe do
+ before do
+ obj = double "Object"
+ expect(obj).to receive_messages(:a => 1, :b => 2)
+ end
+
+ it "will pass" do
+ obj.a && obj.b
+ end
+
+ it "will fail" do
+ obj.a
+ end
+ end
+ example_group.run reporter
@myronmarston

myronmarston Aug 9, 2013

Owner

I'm a fan of creating example groups and running them from within a spec like this for specs in rspec-core, but it has a bunch of sandboxing to assist. We don't have that here. In addition, it seems kinda complicated/unnecessary. Elsewhere we use an expression like:

expect { verify obj }.to raise_error(RSpec::Mocks::MockExpectationError)

...or:

expect { verify obj }.not_to raise_error

You can also use verify_all in place of verify obj. For an example of this, see receive_spec.rb.

If you switch to this way of testing it you don't need to mess with mocking the reporter.

@JonRowe

JonRowe Aug 10, 2013

Owner

I've refactored these, take a look?

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+ end
+
+ it "will fail" do
+ end
+ end
+ example_group.run reporter
+ end
+
+ it "complains if a block is given" do
+ expect do
+ expect(double).to receive_messages(:a => 1) { "implementation" }
+ end.to raise_error "Implementation blocks arn't supported with `receive_messages`"
+ end
+ end
+ end
+end
@myronmarston

myronmarston Aug 9, 2013

Owner

I don't see anything here for allow_any_instance_of or expect_any_instance_of. Those should be supported as well, right?

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 9, 2013

lib/rspec/mocks/syntax.rb
@@ -92,6 +92,11 @@ def receive(method_name, &block)
Matchers::Receive.new(method_name, block)
end
+ def receive_messages(method_value_hash)
+ raise "Implementation blocks arn't supported with `receive_messages`" if block_given?
+ Matchers::ReceivesMessages.new(method_value_hash)
+ end
+
@myronmarston

myronmarston Aug 9, 2013

Owner

Would be good to add yard docs for this method. Due to the way these methods get defined at runtime, yard doesn't pick them up if we put the docs directly above them. Instead, see the bottom of this file -- the docs are there.

@JonRowe

JonRowe Aug 10, 2013

Owner

Yup, I'll make sure to do that.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 10, 2013

lib/rspec/mocks/syntax.rb
@@ -321,6 +327,18 @@ def self.default_should_syntax_host
# expect(obj).to receive(:hello).with("world").exactly(3).times
#
# @note This is only available when you have enabled the `expect` syntax.
+ #
+ # @method receive_messages
+ # Used to specify multiple messages that you expect (or allow) an
@myronmarston

myronmarston Aug 10, 2013

Owner

This implies it's only for the case when you have multiple messages. It's also for the case when you have a single message/return value pair and want the convenience of the hash syntax.

@JonRowe

JonRowe Aug 10, 2013

Owner

Refactored, WDYT now?

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 10, 2013

lib/rspec/mocks/syntax.rb
@@ -321,6 +327,18 @@ def self.default_should_syntax_host
# expect(obj).to receive(:hello).with("world").exactly(3).times
#
# @note This is only available when you have enabled the `expect` syntax.
+ #
+ # @method receive_messages
+ # Used to specify multiple messages that you expect (or allow) an
+ # object to receive. The method takes a hash of method names and
+ # their respective return values. Unlike `receive` block implementations
@myronmarston

myronmarston Aug 10, 2013

Owner

Rather than saying a "hash of method names and their respective return values", can we say a "hash of message/return-value pairs"? We use the term "message" pretty consistently rather than "method name" and it's more consistent with the "receive" phrasing we've chosen to use.

@JonRowe

JonRowe Aug 10, 2013

Owner

I was looking for message name, which didn't fit, so went to method name, reverted to message.

Owner

JonRowe commented Aug 12, 2013

As promised, refactored to use the improvements from #401

@myronmarston myronmarston commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/framework.rb
@@ -28,6 +28,7 @@
require 'rspec/mocks/mutate_const'
require 'rspec/mocks/matchers/have_received'
require 'rspec/mocks/matchers/receive'
+require 'rspec/mocks/matchers/receives_messages'
@myronmarston

myronmarston Aug 13, 2013

Owner

The matcher is receive_messages not receives_messages....shouldn't the file be named accordingly?

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/syntax.rb
@@ -92,6 +92,11 @@ def receive(method_name, &block)
Matchers::Receive.new(method_name, block)
end
+ def receive_messages(method_value_hash)
+ raise "Implementation blocks arn't supported with `receive_messages`" if block_given?
@myronmarston

myronmarston Aug 13, 2013

Owner

s/arn't/aren't/

@JonRowe

JonRowe Aug 13, 2013

Owner

Good catch

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/syntax.rb
@@ -92,6 +92,11 @@ def receive(method_name, &block)
Matchers::Receive.new(method_name, block)
end
+ def receive_messages(method_value_hash)
@myronmarston

myronmarston Aug 13, 2013

Owner

For consistency, I think it'd be good to name this message_return_value_pairs to align with receive_messages. Thoughts?

@JonRowe

JonRowe Aug 13, 2013

Owner

Agree on message_return_value but sticking with hash over pairs as I feel it makes it clearer what the argument is expected to be.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/syntax.rb
@@ -321,6 +327,19 @@ def self.default_should_syntax_host
# expect(obj).to receive(:hello).with("world").exactly(3).times
#
# @note This is only available when you have enabled the `expect` syntax.
+ #
+ # @method receive_messages
+ # Shorthand syntax used to setup message(s), and their return value(s),
+ # that you expect or allow an object to receive. The method takes a hash
+ # of messages and their respective return values. Unlike `receive` block
@myronmarston

myronmarston Aug 13, 2013

Owner

I think that this:

Unlike `recieve` block

Should be:

Unlike `receive`, block

(The comma seems important for it to read correctly...)

@JonRowe

JonRowe Aug 13, 2013

Owner

Yep, agree

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/matchers/receives_messages.rb
+module RSpec
+ module Mocks
+ module Matchers
+ class ReceivesMessages
+
+ def initialize(message_value_hash)
+ @message_value_hash = message_value_hash
+ end
+
+ def setup_expectation(subject, &block)
+ map_to proxy_on(subject), :add_simple_expectation
+ end
+ alias matches? setup_expectation
+ alias does_not_match? setup_expectation
+ alias setup_negative_expectation setup_expectation
+ alias setup_allowance setup_expectation
@myronmarston

myronmarston Aug 13, 2013

Owner

Why are all these aliased to setup_expectation?

  • setup_allowance is overriden below.
  • The negative ones (does_not_match and setup_negative_expectation) seem wrong to be the same thing as the positive case.

Actually, I'm not sure this should support the negative case at all...what would this mean?

expect(object).not_to receive_messages(foo: 3, bar: 25)

It's confusing since return values don't make sense for the negative case. Would be good to spec this, of course.

@JonRowe

JonRowe Aug 13, 2013

Owner

They were supposed to be the same, but I misunderstood how the negation was being applied, I've dropped the negative case since I don't think it makes sense (and I don't believe it's useful to have such negative assertions anyway, I prefer just not stubbing the methods)

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/matchers/receives_messages.rb
+
+ def any_instance_of(subject)
+ ::RSpec::Mocks.any_instance_recorder_for(subject)
+ end
+
+ def map_to(host, method_name)
+ @message_value_hash.each do |message, value|
+ host.__send__(method_name, message.to_sym, value)
+ end
+ end
+
+ def map_to_as_chain(host, method_name, *args)
+ @message_value_hash.each do |message, value|
+ host.__send__(method_name, message, *args).and_return(value)
+ end
+ end
@myronmarston

myronmarston Aug 13, 2013

Owner

I find the names map_to and map_to_as_chain to be confusing...it's not clear to me what they are supposed to mean. Can you explain them? Or maybe a different name is better...I'm unsure right now.

@JonRowe

JonRowe Aug 13, 2013

Owner

I've refactored this to make it clearer

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/error_generator.rb
@@ -48,6 +48,11 @@ def raise_expectation_error(message, expected_received_count, argument_list_matc
end
# @private
+ def raise_simple_expectation_error(message)
+ __raise "(#{intro}).#{message} expected with any arguments"
@myronmarston

myronmarston Aug 13, 2013

Owner

Is this the same message phrasing as has been returned for a normal expectation?

@myronmarston

myronmarston Aug 13, 2013

Owner

Actually, is there even a need for a different method/message here?

@JonRowe

JonRowe Aug 13, 2013

Owner

The more complex error generator method has more things that the SimpleMessageExpectation doesn't know about, it seemed cleaner to be able to describe a simpler message, the language could use some tweaks though.

@JonRowe

JonRowe Aug 13, 2013

Owner

Ok, I figured out the proper error generator :), so this is gone.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/matchers/receives_messages.rb
@@ -0,0 +1,59 @@
+module RSpec
+ module Mocks
+ module Matchers
+ class ReceivesMessages
@myronmarston

myronmarston Aug 13, 2013

Owner

I think the class should be ReceiveMessages since the matcher method is receive_messages.

@JonRowe

JonRowe Aug 13, 2013

Owner

I though it read better, but I'm not overly attached to it, changed.

@myronmarston myronmarston and 1 other commented on an outdated diff Aug 13, 2013

lib/rspec/mocks/targets.rb
@@ -11,8 +11,9 @@ def initialize(target)
def self.delegate_to(matcher_method, options = {})
method_name = options.fetch(:from) { :to }
define_method(method_name) do |matcher, &block|
- unless Matchers::Receive === matcher
- raise UnsupportedMatcherError, "only the `receive` matcher is supported " +
+ unless Matchers::Receive === matcher || Matchers::ReceivesMessages === matcher
+ raise UnsupportedMatcherError,
+ "only the `receive` or `receive_messages` matchers are supported " +
@myronmarston

myronmarston Aug 13, 2013

Owner

I think you want "and" not "or" here....after all, both matchers are supported.

@JonRowe

JonRowe Aug 13, 2013

Owner

I prefer "or" as either matcher is supported... maybe it should read "and/or"...

@myronmarston myronmarston commented on the diff Aug 13, 2013

lib/rspec/mocks/matchers/receive_messages.rb
+
+ private
+
+ def proxy_on(subject)
+ ::RSpec::Mocks.proxy_for(subject)
+ end
+
+ def any_instance_of(subject)
+ ::RSpec::Mocks.any_instance_recorder_for(subject)
+ end
+
+ def each_message_on(host)
+ @message_return_value_hash.each do |message, value|
+ yield host, message, value
+ end
+ end
@myronmarston

myronmarston Aug 13, 2013

Owner

This is much more readable/understandable than the map_on thing you had before. Thanks!

One suggestion, though: given that the host argument is just yielded back to the caller, it seems a bit unnecessary. The name of this method also suggests that this method does something with it or uses it to determine what messages to yield.

Is there a reason you chose to pass it through? I'd probably (slightly) favor just putting a host = blah line at the call sites and then change this to each_message.

@JonRowe

JonRowe Aug 13, 2013

Owner

I liked the look of passing it in and out rather than having a local variable, that was all

Owner

JonRowe commented Aug 13, 2013

Whats the story with #401 and #404 and this?

Member

xaviershay commented Aug 14, 2013

#401 is broken, if it needs to be re-implemented this patch will have to change too. I need to understand how this PR interacts with it. Didn't leave myself enough time to do that tonight, unfortunately.

@myronmarston myronmarston commented on an outdated diff Aug 16, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+ let(:obj) { Object.new }
+
+ it "sets up multiple expectations" do
+ expect_any_instance_of(Object).to receive_messages(:a => 1, :b => 2)
+ obj.a
+ expect { RSpec::Mocks.space.verify_all }.to raise_error RSpec::Mocks::MockExpectationError
+ end
+
+ it "complains if a block is given" do
+ expect {
+ expect_any_instance_of(Object).to receive_messages(:a => 1) { "implementation" }
+ }.to raise_error "Implementation blocks aren't supported with `receive_messages`"
+ end
+ end
+ end
+end
@myronmarston

myronmarston Aug 16, 2013

Owner

I'd like to see some specs that document how .not_to receive_messages is handled. Can you add those?

@myronmarston myronmarston commented on the diff Aug 16, 2013

lib/rspec/mocks/method_double.rb
@@ -135,18 +135,28 @@ def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &imp
# A simple stub can only return a concrete value for a message, and
# cannot match on arguments. It is used as an optimization over
- # `add_stub` where it is known in advance that this is all that will be
- # required of a stub, such as when passing attributes to the `double`
- # example method. They do not stash or restore existing method
+ # `add_stub` / `add_expectation` where it is known in advance that this
+ # is all that will be required of a stub, such as when passing attributes
+ # to the `double` example method. They do not stash or restore existing method
# definitions.
@myronmarston

myronmarston Aug 16, 2013

Owner

As documented here, simple stubs/expectations do not stash or restore existing method defs. This is OK for test doubles (the original case @xaviershay had in mind for simple stubs) but not for partial mocks (as will be used with this feature). Can you add some specs (which should fail) about the resetting on a partial mocks, and then fix it? I suggested one possible fix here:

In #399 we'd like to use the new simple stub stuff, but I guess a simple stub on a partial mock is a "slightly less simple stub" since it needs to be reset properly. I think that should be taken care of as part of that ticket since it's not needed with what we currently have. Here's an idea for how to do that: In TestDouble#__build_mock_proxy it will instantiate a subclass of Proxy that overrides add_simple_stub to pass a support_reset: false flag to add_simple_stub. For other proxies (e.g. for a partial mock) it'll pass true for this flag, and based on the flag it'll either do configure_method or define_proxy_method; @needs_restoration = false. I think that'll work.

Owner

JonRowe commented Aug 19, 2013

@myronmarston I've added in specs covering what happens when you attempt to use the negative (I'm disallowing it) but I'm having trouble consistently getting a partial mocking failure. What should trigger it? I don't want to create order dependant specs...

Owner

JonRowe commented Sep 3, 2013

Ping! Any advice?

@myronmarston myronmarston and 1 other commented on an outdated diff Sep 4, 2013

lib/rspec/mocks/message_expectation.rb
end
def matches?(message, *_)
- self.message == message
+ @message == message
+ end
+
+ def called_max_times?
+ false
+ end
+
+ def verify_messages_received
+ unless @received
+ @error_generator.raise_expectation_error(@message, 1, ArgumentListMatcher::MATCH_ALL, 0, nil)
+ end
+ rescue RSpec::Mocks::MockExpectationError => error
+ error.backtrace.insert(0, @backtrace_line)
+ Kernel::raise error
end
@myronmarston

myronmarston Sep 4, 2013

Owner

The logic in this method (particularly the rescue and backtrace insertion bit) is very similar to what's already here:

https://github.com/rspec/rspec-mocks/blob/3ac6f4e0155db12aa1ecbfe20d710e2d90c581fa/lib/rspec/mocks/message_expectation.rb#L247-L252

Can some common logic be extracted?

@JonRowe

JonRowe Sep 9, 2013

Owner

Refactored

@myronmarston myronmarston and 1 other commented on an outdated diff Sep 4, 2013

lib/rspec/mocks/matchers/receive_messages.rb
@@ -0,0 +1,64 @@
+module RSpec
+ module Mocks
+ module Matchers
+ class ReceiveMessages
+
+ def initialize(message_return_value_hash)
+ @message_return_value_hash = message_return_value_hash
+ @backtrace_line = CallerFilter.first_non_rspec_line
+ end
+
+ def name
+ "receive_messages"
+ end
+
+ def setup_expectation(subject, &block)
@myronmarston

myronmarston Sep 4, 2013

Owner

I noticed you are capturing the block into a Proc variable but not using it. Capturing the block like this has a perf cost; see this conversation thread:

rspec/rspec-core#1057 (comment)

That said, I noticed in the definition of receive_messages that you raise an error if a block is passed, but that will only get triggered if the block is passed with curlies -- if it is passed with do/end, it'll get passed here instead. For consistency, it should raise an error in both cases if we're going to bother with the error at all.

You know, I think you can get by with just using block_given? there (and here) to avoid the cost of reifying the block.

@JonRowe

JonRowe Sep 9, 2013

Owner

Ah I didn't know there was a performance issue, this was cargo culted interface from the other matcher, the syntax def already uses block_given? and I'm currently having issues finding where the do/end block gets lost at the moment though.

@myronmarston myronmarston and 1 other commented on an outdated diff Sep 4, 2013

lib/rspec/mocks/syntax.rb
@@ -321,6 +327,19 @@ def self.default_should_syntax_host
# expect(obj).to receive(:hello).with("world").exactly(3).times
#
# @note This is only available when you have enabled the `expect` syntax.
+ #
+ # @method receive_messages
+ # Shorthand syntax used to setup message(s), and their return value(s),
+ # that you expect or allow an object to receive. The method takes a hash
+ # of messages and their respective return values. Unlike `receive`, block
+ # implementations are not supported, neither is the fluent interface.
@myronmarston

myronmarston Sep 4, 2013

Owner

There's something about this last sentence that reads a bit awkwardly to me. What do you think about this instead?

Unlike with `receive`, you cannot apply further customizations using
a block or the fluent interface.

Thoughts?

@JonRowe

JonRowe Sep 9, 2013

Owner

Yeah I like that better, will update.

@myronmarston myronmarston and 1 other commented on an outdated diff Sep 4, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+
+ describe "expect(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:obj) { double "Object" }
+
+ let(:expectation_error) do
+ failure = nil
+ begin
+ RSpec::Mocks.space.verify_all
+ rescue RSpec::Mocks::MockExpectationError => error
+ failure = error
+ end
+ failure
+ end
+
+
+ it "sets up multiple expectations" do
@myronmarston

myronmarston Sep 4, 2013

Owner

Looks like there's an extra line break that's not needed here. (I don't ever really see a need for 2 blank lines in a row in a spec file like this).

@JonRowe

JonRowe Sep 9, 2013

Owner

It's a typo, removed.

@myronmarston myronmarston commented on an outdated diff Sep 4, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+ it 'fails with a sensible message' do
+ expect(obj).to receive_messages(:a => 1, :b => 2)
+ obj.b
+ expect(expectation_error.to_s).to eq %Q{(Double "Object").a(no args)\n expected: 1 time with any arguments\n received: 0 times}
+ end
+
+ it 'fails with the correct location' do
+ expect(obj).to receive_messages(:a => 1, :b => 2); line = __LINE__
+ expect(expectation_error.backtrace[0]).to match /#{__FILE__}:#{line}/
+ end
+
+ it "complains if a block is given" do
+ expect {
+ expect(double).to receive_messages(:a => 1) { "implementation" }
+ }.to raise_error "Implementation blocks aren't supported with `receive_messages`"
+ end
@myronmarston

myronmarston Sep 4, 2013

Owner

As I mentioned above, I think you're missing a spec for the do/end block case.

@myronmarston myronmarston commented on the diff Sep 4, 2013

spec/rspec/mocks/matchers/receive_messages_spec.rb
+ raise_error "`allow_any_instance_of(...).to_not receive_messages` is not supported "+
+ "since it doesn't really make sense. What would it even mean?"
+ )
+ end
+ specify "expect(...).to_not receive_messages(:a => 1, :b => 2)" do
+ expect { expect(obj).to_not receive_messages(:a => 1, :b => 2) }.to(
+ raise_error "`expect(...).to_not receive_messages` is not supported "+
+ "since it doesn't really make sense. What would it even mean?"
+ )
+ end
+ specify "expect_any_instance_of(...).to_not receive_messages(:a => 1, :b => 2)" do
+ expect { expect_any_instance_of(obj).to_not receive_messages(:a => 1, :b => 2) }.to(
+ raise_error "`expect_any_instance_of(...).to_not receive_messages` is not supported "+
+ "since it doesn't really make sense. What would it even mean?"
+ )
+ end
@myronmarston

myronmarston Sep 4, 2013

Owner

I think it makes more semantic sense to use example rather than specify for these examples. In general, here's when I use it, specify and example:

  • I use it when I can write an english sentence fragment that reads nicely off of "it".
  • I use specify when I'm writing an English sentence fragment but it doesn't read well off of "it". (e.g. specify "the user can access the page" rather than it "the user can access the page").
  • I use example when my context has already named the behavior and I'm simply listing different examples where that behavior holds.

This is the latter case, IMO.

Also, blank lines between examples would be nice :).

Owner

myronmarston commented Sep 4, 2013

Here's an example of a spec that fails (but should pass) for the partial mock case:

module RSpec
  module Mocks
    describe "allow(...).to receive_messages(:a => 1, :b => 2)" do
      context "on a partial mock object" do
        it 'gets reset properly after the example' do
          obj = Object.new
          def obj.a; "original"; end

          allow(obj).to receive_messages(:a => 1, :b => 2)

          expect {
            reset obj
          }.to change { obj.a }.from(1).to("original")
        end
      end
    end
  end
end

Also, this is now at 21 commits and counting. There's a lot of noise in there. It would be nice if this got squashed into a more reasonable number of commits before we merge this :).

JonRowe added some commits Aug 9, 2013

Owner

JonRowe commented Sep 9, 2013

I'll squash this before we merge it but I'm leaving the history until I'm ready for that :)

@myronmarston myronmarston and 1 other commented on an outdated diff Sep 9, 2013

lib/rspec/mocks/message_expectation.rb
@@ -585,5 +603,17 @@ def cannot_modify_further_error
"to call the original implementation, and cannot be modified further."
end
end
+
+ # Insert original locations into stacktraces
+ # @api private
+ class BacktrackRestore
@myronmarston

myronmarston Sep 9, 2013

Owner

BacktrackRestore is a very odd name. I have no idea what the name has to do with what the code below does...

I don't have a better name idea yet, though :(.

@JonRowe

JonRowe Sep 9, 2013

Owner

I've changed to InsertOntoBacktrace.line

Coverage Status

Coverage decreased (-0.06%) when pulling c3bdeff on receive_messages into 3ac6f4e on master.

Owner

JonRowe commented Sep 9, 2013

Ok, this now resets partial mocks properly (it was actually a fairly trivial change as we have a separate PartialMockProxy to handle this).

One thing I haven't done, and this is deliberate cause my head hurts just thinking about it, is handle resetting 'any_instance' stubs/expectations, because they are already reset entirely separately from everything else, and I'm using the existing stub/expectation functionality for those (it's not new code).

They already don't reset cleanly, so I figure that's a new issue.

Owner

myronmarston commented Sep 10, 2013

They already don't reset cleanly, so I figure that's a new issue.

That's news to me. Can you open an rspec-mocks issue includes a code snippet that demonstrates the problem?

myronmarston merged commit 69954e9 into master Sep 12, 2013

1 check passed

default The Travis CI build passed
Details

myronmarston deleted the receive_messages branch Sep 12, 2013

Owner

myronmarston commented Sep 12, 2013

Merged. Thanks @JonRowe!

Owner

JonRowe commented Sep 12, 2013

Weren't we going to squash this? :P

Owner

myronmarston commented Sep 12, 2013

Oh yeah.....too late now, I guess.

Owner

JonRowe commented Sep 12, 2013

Yup, oh well :)

👍

yujinakayama referenced this pull request in yujinakayama/transpec Sep 24, 2013

Closed

Support conversion to `allow(…).to receive_messages` #6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment