Permalink
Browse files

Merge pull request #399 from rspec/receive_messages

Allow multiple message allowances/expectations via `receive_messages`
  • Loading branch information...
2 parents b34488c + c3bdeff commit 69954e944cad709dcad075365ade7b317a63f427 @myronmarston myronmarston committed Sep 12, 2013
View
2 Changelog.md
@@ -30,6 +30,8 @@ Enhancements:
attributes. (Xavier Shay)
* Default value of `transfer_nested_constants` option for constant stubbing can
be configured. (Xavier Shay)
+* Messages can be allowed or expected on in bulk via
+ `receive_messages(:message => :value)` (Jon Rowe)
Deprecations:
View
1 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/receive_messages'
require 'rspec/mocks/stub_chain'
require 'rspec/mocks/targets'
require 'rspec/mocks/syntax'
View
4 lib/rspec/mocks/matchers/have_received.rb
@@ -13,6 +13,10 @@ def initialize(method_name, &block)
@subject = nil
end
+ def name
+ "have_received"
+ end
+
def matches?(subject, &block)
@block ||= block
@subject = subject
View
8 lib/rspec/mocks/matchers/receive.rb
@@ -11,9 +11,11 @@ def initialize(message, block)
# reports an extra "in `new'" line in the backtrace that the
# others do not include. The safest way to find the right
# line is to search for the first line BEFORE rspec/mocks/syntax.rb.
- @backtrace_line = caller.find do |line|
- !line.split(':').first.end_with?('rspec/mocks/syntax.rb')
- end
+ @backtrace_line = CallerFilter.first_non_rspec_line
+ end
+
+ def name
+ "receive"
end
def setup_expectation(subject, &block)
View
72 lib/rspec/mocks/matchers/receive_messages.rb
@@ -0,0 +1,72 @@
+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)
+ warn_about_block if block_given?
+ each_message_on( proxy_on(subject) ) do |host, message, return_value|
+ host.add_simple_expectation(message, return_value, @backtrace_line)
+ end
+ end
+ alias matches? setup_expectation
+
+ 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
+
+ def setup_allowance(subject)
+ warn_about_block if block_given?
+ each_message_on( proxy_on(subject) ) do |host, message, return_value|
+ host.add_simple_stub(message, return_value)
+ end
+ end
+
+ def setup_any_instance_expectation(subject)
+ warn_about_block if block_given?
+ each_message_on( any_instance_of(subject) ) do |host, message, return_value|
+ host.should_receive(message).and_return(return_value)
+ end
+ end
+
+ def setup_any_instance_allowance(subject)
+ warn_about_block if block_given?
+ any_instance_of(subject).stub(@message_return_value_hash)
+ end
+
+ def warn_about_block
+ raise "Implementation blocks aren't supported with `receive_messages`"
+ end
+
+ 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
+
+ end
+ end
+ end
+end
View
44 lib/rspec/mocks/message_expectation.rb
@@ -5,13 +5,32 @@ module Mocks
# for a message. While this same effect can be achieved using a standard
# MessageExpecation, this version is much faster and so can be used as an
# optimization.
- SimpleMessageExpectation = Struct.new(:message, :response) do
+ class SimpleMessageExpectation
+
+ def initialize(message, response, error_generator, backtrace_line = nil)
+ @message, @response, @error_generator, @backtrace_line = message, response, error_generator, backtrace_line
+ @received = false
+ end
+
def invoke(*_)
- response
+ @received = true
+ @response
end
def matches?(message, *_)
- self.message == message
+ @message == message
+ end
+
+ def called_max_times?
+ false
+ end
+
+ def verify_messages_received
+ InsertOntoBacktrace.line(@backtrace_line) do
+ unless @received
+ @error_generator.raise_expectation_error(@message, 1, ArgumentListMatcher::MATCH_ALL, 0, nil)
+ end
+ end
end
end
@@ -245,10 +264,9 @@ def matches_name_but_not_args(message, *args)
# @private
def verify_messages_received
- generate_error unless expected_messages_received? || failed_fast?
- rescue RSpec::Mocks::MockExpectationError => error
- error.backtrace.insert(0, @expected_from)
- Kernel::raise error
+ InsertOntoBacktrace.line(@expected_from) do
+ generate_error unless expected_messages_received? || failed_fast?
+ end
end
# @private
@@ -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 InsertOntoBacktrace
+ def self.line(location)
+ yield
+ rescue RSpec::Mocks::MockExpectationError => error
+ error.backtrace.insert(0, location)
+ Kernel::raise error
+ end
+ end
+
end
end
View
22 lib/rspec/mocks/method_double.rb
@@ -139,18 +139,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.
#
# @private
def add_simple_stub(method_name, response)
+ setup_simple_method_double method_name, response, stubs
+ end
+
+ # @private
+ def add_simple_expectation(method_name, response, error_generator, backtrace_line)
+ setup_simple_method_double method_name, response, expectations, error_generator, backtrace_line
+ end
+
+ # @private
+ def setup_simple_method_double(method_name, response, collection, error_generator = nil, backtrace_line = nil)
define_proxy_method
- stub = SimpleMessageExpectation.new(method_name, response)
- stubs.unshift stub
- stub
+ me = SimpleMessageExpectation.new(method_name, response, error_generator, backtrace_line)
+ collection.unshift me
+ me
end
# @private
View
17 lib/rspec/mocks/proxy.rb
@@ -60,6 +60,11 @@ def add_message_expectation(location, method_name, opts={}, &block)
end
# @private
+ def add_simple_expectation(method_name, response, location)
+ method_double[method_name].add_simple_expectation method_name, response, @error_generator, location
+ end
+
+ # @private
def build_expectation(method_name)
meth_double = method_double[method_name]
@@ -248,6 +253,18 @@ def method_handle_for(message)
nil
end
+ # @private
+ def add_simple_expectation(method_name, response, location)
+ method_double[method_name].configure_method
+ super
+ end
+
+ # @private
+ def add_simple_stub(method_name, response)
+ method_double[method_name].configure_method
+ super
+ end
+
private
def any_instance_class_recorder_observing_method?(klass, method_name)
View
21 lib/rspec/mocks/syntax.rb
@@ -118,6 +118,12 @@ def receive(method_name, &block)
Matchers::Receive.new(method_name, block)
end
+ def receive_messages(message_return_value_hash)
+ matcher = Matchers::ReceiveMessages.new(message_return_value_hash)
+ matcher.warn_about_block if block_given?
+ matcher
+ end
+
def allow(target)
AllowanceTarget.new(target)
end
@@ -145,6 +151,7 @@ def self.disable_expect(syntax_host = ::RSpec::Mocks::ExampleMethods)
syntax_host.class_exec do
undef receive
+ undef receive_messages
undef allow
undef expect_any_instance_of
undef allow_any_instance_of
@@ -347,6 +354,20 @@ 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 with `receive`,
+ # you cannot apply further customizations using a block or the fluent
+ # interface.
+ #
+ # @example
+ #
+ # allow(obj).to receive_messages(:speak => "Hello World")
+ # allow(obj).to receive_messages(:speak => "Hello", :meow => "Meow")
+ #
+ # @note This is only available when you have enabled the `expect` syntax.
end
end
end
View
57 lib/rspec/mocks/targets.rb
@@ -8,28 +8,51 @@ def initialize(target)
@target = target
end
- 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 " +
- "with `#{expression}(...).#{method_name}`, but you have provided: #{matcher}"
+ def self.delegate_to(matcher_method)
+ define_method(:to) do |matcher, &block|
+ unless Matchers::Receive === matcher || Matchers::ReceiveMessages === matcher
+ raise_unsupported_matcher(:to, matcher)
end
+ define_matcher(matcher, matcher_method, &block)
+ end
+ end
- matcher.__send__(matcher_method, @target, &block)
+ def self.delegate_not_to(matcher_method, options = {})
+ method_name = options.fetch(:from)
+ define_method(method_name) do |matcher, &block|
+ case matcher
+ when Matchers::Receive then define_matcher(matcher, matcher_method, &block)
+ when Matchers::ReceiveMessages then raise_negation_unsupported(method_name, matcher)
+ else
+ raise_unsupported_matcher(method_name, matcher)
+ end
end
end
- def self.disallow_negation(method)
- define_method method do |*args|
- raise NegationUnsupportedError,
- "`#{expression}(...).#{method} receive` is not supported since it " +
- "doesn't really make sense. What would it even mean?"
+ def self.disallow_negation(method_name)
+ define_method(method_name) do |matcher, *args|
+ raise_negation_unsupported(method_name, matcher)
end
end
private
+ def define_matcher(matcher, name, &block)
+ matcher.__send__(name, @target, &block)
+ end
+
+ def raise_unsupported_matcher(method_name, matcher)
+ raise UnsupportedMatcherError,
+ "only the `receive` or `receive_messages` matchers are supported " +
+ "with `#{expression}(...).#{method_name}`, but you have provided: #{matcher}"
+ end
+
+ def raise_negation_unsupported(method_name, matcher)
+ raise NegationUnsupportedError,
+ "`#{expression}(...).#{method_name} #{matcher.name}` is not supported since it " +
+ "doesn't really make sense. What would it even mean?"
+ end
+
def expression
self.class::EXPRESSION
end
@@ -45,12 +68,12 @@ class AllowanceTarget < TargetBase
class ExpectationTarget < TargetBase
EXPRESSION = :expect
delegate_to :setup_expectation
- delegate_to :setup_negative_expectation, :from => :not_to
- delegate_to :setup_negative_expectation, :from => :to_not
+ delegate_not_to :setup_negative_expectation, :from => :not_to
+ delegate_not_to :setup_negative_expectation, :from => :to_not
end
class AnyInstanceAllowanceTarget < TargetBase
- EXPRESSION = :expect_any_instance_of
+ EXPRESSION = :allow_any_instance_of
delegate_to :setup_any_instance_allowance
disallow_negation :not_to
disallow_negation :to_not
@@ -59,8 +82,8 @@ class AnyInstanceAllowanceTarget < TargetBase
class AnyInstanceExpectationTarget < TargetBase
EXPRESSION = :expect_any_instance_of
delegate_to :setup_any_instance_expectation
- delegate_to :setup_any_instance_negative_expectation, :from => :not_to
- delegate_to :setup_any_instance_negative_expectation, :from => :to_not
+ delegate_not_to :setup_any_instance_negative_expectation, :from => :not_to
+ delegate_not_to :setup_any_instance_negative_expectation, :from => :to_not
end
end
end
View
140 spec/rspec/mocks/matchers/receive_messages_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+module RSpec
+ module Mocks
+ shared_examples_for "complains when given blocks" do
+ it "complains if a { } block is given" do
+ expect {
+ target.to receive_messages(:a => 1) { "implementation" }
+ }.to raise_error "Implementation blocks aren't supported with `receive_messages`"
+ end
+
+ it "complains if a do; end; block is given" do
+ expect {
+ target.to receive_messages(:a => 1) do
+ "implementation"
+ end
+ }.to raise_error "Implementation blocks aren't supported with `receive_messages`"
+ end
+ end
+
+ shared_examples_for "handles partially mocked objects correctly" do
+ let(:obj) { Struct.new(:a).new('original') }
+
+ it "resets partially mocked objects correctly" do
+ target.to receive_messages(:a => 1, :b => 2)
+
+ expect {
+ reset obj
+ }.to change { obj.a }.from(1).to("original")
+ end
+ end
+
+ describe "allow(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:obj) { double "Object" }
+ let(:target) { allow(obj) }
+
+ 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_behaves_like "complains when given blocks"
+ it_behaves_like "handles partially mocked objects correctly"
+ end
+
+ describe "allow_any_instance_of(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:obj) { Object.new }
+ let(:target) { allow_any_instance_of(Object) }
+
+ it "allows the object to respond to multiple messages" do
+ allow_any_instance_of(Object).to receive_messages(:a => 1, :b => 2)
+ expect(obj.a).to eq 1
+ expect(obj.b).to eq 2
+ end
+
+ it_behaves_like "complains when given blocks"
+ end
+
+ describe "expect(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:obj) { double "Object" }
+ let(:target) { expect(obj) }
+
+ 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
+ expect(obj).to receive_messages(:a => 1, :b => 2)
+ obj.a
+ expect { RSpec::Mocks.space.verify_all }.to raise_error RSpec::Mocks::MockExpectationError
+ end
+
+ 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_behaves_like "complains when given blocks"
+ it_behaves_like "handles partially mocked objects correctly"
+ end
+
+ describe "expect_any_instance_of(...).to receive_messages(:a => 1, :b => 2)" do
+ let(:obj) { Object.new }
+ let(:target) { expect_any_instance_of(Object) }
+
+ 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_behaves_like "complains when given blocks"
+ end
+
+ describe "negative expectation failure" do
+ let(:obj) { Object.new }
+
+ example "allow(...).to_not receive_messages(:a => 1, :b => 2)" do
+ expect { allow(obj).to_not receive_messages(:a => 1, :b => 2) }.to(
+ raise_error "`allow(...).to_not receive_messages` is not supported "+
+ "since it doesn't really make sense. What would it even mean?"
+ )
+ end
+
+ example "allow_any_instance_of(...).to_not receive_messages(:a => 1, :b => 2)" do
+ expect { allow_any_instance_of(obj).to_not receive_messages(:a => 1, :b => 2) }.to(
+ 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
+
+ example "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
+
+ example "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
+ end
+ end
+end
View
2 spec/rspec/mocks/matchers/receive_spec.rb
@@ -265,7 +265,7 @@ def eq(value)
framework.new.instance_exec do
expect(3).to eq(3)
end
- }.to raise_error(/only the `receive` matcher is supported with `expect\(...\).to`/)
+ }.to raise_error(/only the `receive` or `receive_messages` matchers are supported with `expect\(...\).to`/)
end
it 'can toggle the available syntax' do

0 comments on commit 69954e9

Please sign in to comment.