From 3ddd8ab723241724f0978a43267a801276530e95 Mon Sep 17 00:00:00 2001 From: Sam Phippen Date: Wed, 13 Nov 2013 17:54:50 -0500 Subject: [PATCH] Add allow(...).to receive_message_chain --- Changelog.md | 6 + .../message_chains_using_expect.feature | 49 +++++ .../mocks/any_instance/expect_chain_chain.rb | 35 ++++ lib/rspec/mocks/any_instance/recorder.rb | 9 + .../mocks/any_instance/stub_chain_chain.rb | 4 + lib/rspec/mocks/error_generator.rb | 6 +- lib/rspec/mocks/framework.rb | 4 +- lib/rspec/mocks/matchers/receive.rb | 24 +-- .../mocks/matchers/receive_message_chain.rb | 65 ++++++ lib/rspec/mocks/message_chain.rb | 91 +++++++++ lib/rspec/mocks/message_expectation.rb | 57 +++--- lib/rspec/mocks/stub_chain.rb | 51 ----- lib/rspec/mocks/syntax.rb | 36 ++++ lib/rspec/mocks/targets.rb | 20 +- spec/rspec/mocks/and_call_original_spec.rb | 10 +- .../matchers/receive_message_chain_spec.rb | 188 ++++++++++++++++++ spec/spec_helper.rb | 4 + 17 files changed, 559 insertions(+), 100 deletions(-) create mode 100644 features/message_expectations/message_chains_using_expect.feature create mode 100644 lib/rspec/mocks/any_instance/expect_chain_chain.rb create mode 100644 lib/rspec/mocks/matchers/receive_message_chain.rb create mode 100644 lib/rspec/mocks/message_chain.rb delete mode 100644 lib/rspec/mocks/stub_chain.rb create mode 100644 spec/rspec/mocks/matchers/receive_message_chain_spec.rb diff --git a/Changelog.md b/Changelog.md index 3e65af32f..9246368fe 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,12 @@ Bug Fixes: * Fix regression in 3.0.0.beta1 that caused `double("string_name" => :value)` to stop working. (Xavier Shay) +Enhancements: + +* 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). + ### 3.0.0.beta1 / 2013-11-07 [full changelog](http://github.com/rspec/rspec-mocks/compare/v2.99.0.beta1...v3.0.0.beta1) diff --git a/features/message_expectations/message_chains_using_expect.feature b/features/message_expectations/message_chains_using_expect.feature new file mode 100644 index 000000000..2beb9e3d8 --- /dev/null +++ b/features/message_expectations/message_chains_using_expect.feature @@ -0,0 +1,49 @@ +Feature: Message chains in the expect syntax + + You can use `receive_message_chain` to stub nested calls + on both partial and pure mock objects. + + Scenario: allow a chained message + Given a file named "spec/chained_messages.rb" with: + """ruby + describe "a chained message expectation" do + it "passes if the expected number of calls happen" do + d = double + allow(d).to receive_message_chain(:to_a, :length) + + d.to_a.length + end + end + """ + When I run `rspec spec/chained_messages.rb` + Then the output should contain "1 example, 0 failures" + + Scenario: allow a chained message with a return value + Given a file named "spec/chained_messages.rb" with: + """ruby + describe "a chained message expectation" do + it "passes if the expected number of calls happen" do + d = double + allow(d).to receive_message_chain(:to_a, :length).and_return(3) + + expect(d.to_a.length).to eq(3) + end + end + """ + When I run `rspec spec/chained_messages.rb` + Then the output should contain "1 example, 0 failures" + + Scenario: expect a chained message with a return value + Given a file named "spec/chained_messages.rb" with: + """ruby + describe "a chained message expectation" do + it "passes if the expected number of calls happen" do + d = double + expect(d).to receive_message_chain(:to_a, :length).and_return(3) + + expect(d.to_a.length).to eq(3) + end + end + """ + When I run `rspec spec/chained_messages.rb` + Then the output should contain "1 example, 0 failures" diff --git a/lib/rspec/mocks/any_instance/expect_chain_chain.rb b/lib/rspec/mocks/any_instance/expect_chain_chain.rb new file mode 100644 index 000000000..da856efe4 --- /dev/null +++ b/lib/rspec/mocks/any_instance/expect_chain_chain.rb @@ -0,0 +1,35 @@ +module RSpec + module Mocks + module AnyInstance + # @private + class ExpectChainChain < StubChain + def initialize(*args) + super + @expectation_fulfilled = false + end + + def expectation_fulfilled? + @expectation_fulfilled + end + + def playback!(instance) + super.tap { @expectation_fulfilled = true } + end + + private + + def create_message_expectation_on(instance) + ::RSpec::Mocks::ExpectChain.expect_chain_on(instance, *@expectation_args, &@expectation_block) + end + + def invocation_order + @invocation_order ||= { + :and_return => [nil], + :and_raise => [nil], + :and_yield => [nil] + } + end + end + end + end +end diff --git a/lib/rspec/mocks/any_instance/recorder.rb b/lib/rspec/mocks/any_instance/recorder.rb index 9aa842f3b..1a1b7eb20 100644 --- a/lib/rspec/mocks/any_instance/recorder.rb +++ b/lib/rspec/mocks/any_instance/recorder.rb @@ -49,6 +49,15 @@ def stub_chain(*method_names_and_optional_return_values, &block) end end + # @api private + def expect_chain(*method_names_and_optional_return_values, &block) + @expectation_set = true + normalize_chain(*method_names_and_optional_return_values) do |method_name, args| + observe!(method_name) + message_chains.add(method_name, ExpectChainChain.new(self, *args, &block)) + end + end + # Initializes the recording a message expectation to be played back # against any instance of this object that invokes the submitted # method. diff --git a/lib/rspec/mocks/any_instance/stub_chain_chain.rb b/lib/rspec/mocks/any_instance/stub_chain_chain.rb index e16d92af8..d24cfc6c0 100644 --- a/lib/rspec/mocks/any_instance/stub_chain_chain.rb +++ b/lib/rspec/mocks/any_instance/stub_chain_chain.rb @@ -3,6 +3,10 @@ module Mocks module AnyInstance # @private class StubChainChain < StubChain + def initialize(*args) + super + @expectation_fulfilled = false + end private diff --git a/lib/rspec/mocks/error_generator.rb b/lib/rspec/mocks/error_generator.rb index 2a21e9d2e..5c3c24321 100644 --- a/lib/rspec/mocks/error_generator.rb +++ b/lib/rspec/mocks/error_generator.rb @@ -117,9 +117,9 @@ def raise_wrong_arity_error(args_to_yield, arity) end # @private - def raise_only_valid_on_a_partial_mock(method) - __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 test double. `#{method}` is only " + + "available on a partial double." end # @private diff --git a/lib/rspec/mocks/framework.rb b/lib/rspec/mocks/framework.rb index 2a38b85bb..050d0650b 100644 --- a/lib/rspec/mocks/framework.rb +++ b/lib/rspec/mocks/framework.rb @@ -21,6 +21,7 @@ require 'rspec/mocks/any_instance/chain' require 'rspec/mocks/any_instance/stub_chain' require 'rspec/mocks/any_instance/stub_chain_chain' +require 'rspec/mocks/any_instance/expect_chain_chain' require 'rspec/mocks/any_instance/expectation_chain' require 'rspec/mocks/any_instance/message_chains' require 'rspec/mocks/any_instance/recorder' @@ -28,7 +29,8 @@ 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/matchers/receive_message_chain' +require 'rspec/mocks/message_chain' require 'rspec/mocks/targets' require 'rspec/mocks/syntax' require 'rspec/mocks/configuration' diff --git a/lib/rspec/mocks/matchers/receive.rb b/lib/rspec/mocks/matchers/receive.rb index d616a8508..1b5af2d98 100644 --- a/lib/rspec/mocks/matchers/receive.rb +++ b/lib/rspec/mocks/matchers/receive.rb @@ -27,7 +27,7 @@ def setup_expectation(subject, &block) def setup_negative_expectation(subject, &block) # ensure `never` goes first for cases like `never.and_return(5)`, # where `and_return` is meant to raise an error - @recorded_customizations.unshift Customization.new(:never, [], nil) + @recorded_customizations.unshift ExpectationCustomization.new(:never, [], nil) warn_if_any_instance("expect", subject) @@ -56,7 +56,7 @@ def setup_any_instance_allowance(subject, &block) next if method_defined?(method) define_method(method) do |*args, &block| - @recorded_customizations << Customization.new(method, args, block) + @recorded_customizations << ExpectationCustomization.new(method, args, block) self end end @@ -93,18 +93,18 @@ def setup_method_substitute(host, method, block, *args) end expectation end + end + end - class Customization - def initialize(method_name, args, block) - @method_name = method_name - @args = args - @block = block - end + class ExpectationCustomization + def initialize(method_name, args, block) + @method_name = method_name + @args = args + @block = block + end - def playback_onto(expectation) - expectation.__send__(@method_name, *@args, &@block) - end - end + def playback_onto(expectation) + expectation.__send__(@method_name, *@args, &@block) end end end diff --git a/lib/rspec/mocks/matchers/receive_message_chain.rb b/lib/rspec/mocks/matchers/receive_message_chain.rb new file mode 100644 index 000000000..bb54725f5 --- /dev/null +++ b/lib/rspec/mocks/matchers/receive_message_chain.rb @@ -0,0 +1,65 @@ +module RSpec + module Mocks + module Matchers + #@api private + class ReceiveMessageChain + def initialize(chain, &block) + @chain = chain + @block = block + @recorded_customizations = [] + end + + [:and_return, :and_throw, :and_raise, :and_yield, :and_call_original].each do |msg| + define_method(msg) do |*args, &block| + @recorded_customizations << ExpectationCustomization.new(msg, args, block) + self + end + end + + def name + "receive_message_chain" + end + + def setup_allowance(subject, &block) + chain = StubChain.stub_chain_on(subject, *@chain, &(@block || block)) + replay_customizations(chain) + end + + def setup_any_instance_allowance(subject, &block) + recorder = ::RSpec::Mocks.any_instance_recorder_for(subject) + chain = recorder.stub_chain(*@chain, &(@block || block)) + replay_customizations(chain) + end + + def setup_any_instance_expectation(subject, &block) + recorder = ::RSpec::Mocks.any_instance_recorder_for(subject) + chain = recorder.expect_chain(*@chain, &(@block || block)) + replay_customizations(chain) + end + + def setup_expectation(subject, &block) + chain = ExpectChain.expect_chain_on(subject, *@chain, &(@block || block)) + replay_customizations(chain) + end + + def setup_negative_expectation(*args) + 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 + + alias matches? setup_expectation + alias does_not_match? setup_negative_expectation + + private + + def replay_customizations(chain) + @recorded_customizations.each do |customization| + customization.playback_onto(chain) + end + end + end + end + end +end diff --git a/lib/rspec/mocks/message_chain.rb b/lib/rspec/mocks/message_chain.rb new file mode 100644 index 000000000..97f2762a1 --- /dev/null +++ b/lib/rspec/mocks/message_chain.rb @@ -0,0 +1,91 @@ +module RSpec + module Mocks + # @private + class MessageChain + attr_reader :object, :chain, :block + + def initialize(object, *chain, &blk) + @object = object + @chain, @block = format_chain(*chain, &blk) + end + + # @api private + def setup_chain + if chain.length > 1 + if matching_stub = find_matching_stub + chain.shift + chain_on(matching_stub.invoke(nil), *chain, &@block) + elsif matching_expectation = find_matching_expectation + chain.shift + chain_on(matching_expectation.invoke_without_incrementing_received_count(nil), *chain, &@block) + else + next_in_chain = Double.new + expectation(object, chain.shift, next_in_chain) + chain_on(next_in_chain, *chain, &@block) + end + else + ::RSpec::Mocks.allow_message(object, chain.shift, {}, &block) + end + end + + private + + def expectation(object, message, returned_object) + raise NotImplementedError.new + end + + def chain_on(object, *chain, &block) + initialize(object, *chain, &block) + setup_chain + end + + def format_chain(*chain, &blk) + if Hash === chain.last + hash = chain.pop + hash.each do |k,v| + chain << k + blk = lambda { v } + end + end + return chain.join('.').split('.'), blk + end + + def find_matching_stub + ::RSpec::Mocks.proxy_for(object). + __send__(:find_matching_method_stub, chain.first.to_sym) + end + + def find_matching_expectation + ::RSpec::Mocks.proxy_for(object). + __send__(:find_matching_expectation, chain.first.to_sym) + end + end + + # @private + class ExpectChain < MessageChain + # @api private + def self.expect_chain_on(object, *chain, &blk) + new(object, *chain, &blk).setup_chain + end + + private + + def expectation(object, message, returned_object) + ::RSpec::Mocks.expect_message(object, message, {}) { returned_object } + end + end + + # @private + class StubChain < MessageChain + def self.stub_chain_on(object, *chain, &blk) + new(object, *chain, &blk).setup_chain + end + + private + + def expectation(object, message, returned_object) + ::RSpec::Mocks.allow_message(object, message, {}) { returned_object } + end + end + end +end diff --git a/lib/rspec/mocks/message_expectation.rb b/lib/rspec/mocks/message_expectation.rb index 15895010c..46a376685 100644 --- a/lib/rspec/mocks/message_expectation.rb +++ b/lib/rspec/mocks/message_expectation.rb @@ -139,7 +139,7 @@ def yield_receiver_to_implementation_block? # Tells the object to delegate to the original unmodified method # when it receives the message. # - # @note This is only available on partial mock objects. + # @note This is only available on partial doubles. # # @example # @@ -149,7 +149,7 @@ def yield_receiver_to_implementation_block? # expect(counter.count).to eq(original_count + 1) def and_call_original if RSpec::Mocks::TestDouble === @method_double.object - @error_generator.raise_only_valid_on_a_partial_mock(:and_call_original) + @error_generator.raise_only_valid_on_a_partial_double(:and_call_original) else if implementation.inner_action RSpec.warning("You're overriding a previous implementation for this stub") @@ -224,29 +224,12 @@ def matches?(message, *args) # @private def invoke(parent_stub, *args, &block) - if yield_receiver_to_implementation_block? - args.unshift(orig_object) - end - - if negative? || ((@exactly || @at_most) && (@actual_received_count == @expected_received_count)) - @actual_received_count += 1 - @failed_fast = true - #args are the args we actually received, @argument_list_matcher is the - #list of args we were expecting - @error_generator.raise_expectation_error(@message, @expected_received_count, @argument_list_matcher, @actual_received_count, expectation_count_type, *args) - end - - @order_group.handle_order_constraint self + invoke_incrementing_actual_calls_by(1, parent_stub, *args, &block) + end - begin - if implementation.present? - implementation.call(*args, &block) - elsif parent_stub - parent_stub.invoke(nil, *args, &block) - end - ensure - @actual_received_count += 1 - end + # @private + def invoke_without_incrementing_received_count(parent_stub, *args, &block) + invoke_incrementing_actual_calls_by(0, parent_stub, *args, &block) end # @private @@ -495,6 +478,32 @@ def increase_actual_received_count! private + def invoke_incrementing_actual_calls_by(increment, parent_stub, *args, &block) + if yield_receiver_to_implementation_block? + args.unshift(orig_object) + end + + if negative? || ((@exactly || @at_most) && (@actual_received_count == @expected_received_count)) + @actual_received_count += increment + @failed_fast = true + #args are the args we actually received, @argument_list_matcher is the + #list of args we were expecting + @error_generator.raise_expectation_error(@message, @expected_received_count, @argument_list_matcher, @actual_received_count, expectation_count_type, *args) + end + + @order_group.handle_order_constraint self + + begin + if implementation.present? + implementation.call(*args, &block) + elsif parent_stub + parent_stub.invoke(nil, *args, &block) + end + ensure + @actual_received_count += increment + end + end + def failed_fast? @failed_fast end diff --git a/lib/rspec/mocks/stub_chain.rb b/lib/rspec/mocks/stub_chain.rb deleted file mode 100644 index e45e0b8d1..000000000 --- a/lib/rspec/mocks/stub_chain.rb +++ /dev/null @@ -1,51 +0,0 @@ -module RSpec - module Mocks - # @private - class StubChain - def self.stub_chain_on(object, *chain, &blk) - new(object, *chain, &blk).stub_chain - end - - attr_reader :object, :chain, :block - - def initialize(object, *chain, &blk) - @object = object - @chain, @block = format_chain(*chain, &blk) - end - - def stub_chain - if chain.length > 1 - if matching_stub = find_matching_stub - chain.shift - matching_stub.invoke(nil).stub_chain(*chain, &block) - else - next_in_chain = Double.new - object.stub(chain.shift) { next_in_chain } - StubChain.stub_chain_on(next_in_chain, *chain, &block) - end - else - object.stub(chain.shift, &block) - end - end - - private - - def format_chain(*chain, &blk) - if Hash === chain.last - hash = chain.pop - hash.each do |k,v| - chain << k - blk = lambda { v } - end - end - return chain.join('.').split('.'), blk - end - - def find_matching_stub - ::RSpec::Mocks.proxy_for(object). - __send__(:find_matching_method_stub, chain.first.to_sym) - end - end - end -end - diff --git a/lib/rspec/mocks/syntax.rb b/lib/rspec/mocks/syntax.rb index 3d0714c04..4a75a7b0e 100644 --- a/lib/rspec/mocks/syntax.rb +++ b/lib/rspec/mocks/syntax.rb @@ -124,6 +124,10 @@ def receive_messages(message_return_value_hash) matcher end + def receive_message_chain(*messages, &block) + Matchers::ReceiveMessageChain.new(messages, &block) + end + def allow(target) AllowanceTarget.new(target) end @@ -152,6 +156,7 @@ def self.disable_expect(syntax_host = ::RSpec::Mocks::ExampleMethods) syntax_host.class_exec do undef receive undef receive_messages + undef receive_message_chain undef allow undef expect_any_instance_of undef allow_any_instance_of @@ -368,6 +373,37 @@ def self.default_should_syntax_host # allow(obj).to receive_messages(:speak => "Hello", :meow => "Meow") # # @note This is only available when you have enabled the `expect` syntax. + # + # @method receive_message_chain + # @overload receive_message_chain(method1, method2) + # @overload receive_message_chain("method1.method2") + # @overload receive_message_chain(method1, method_to_value_hash) + # + # stubs/mocks a chain of messages on an object or test double. + # + # ## Warning: + # + # Chains can be arbitrarily long, which makes it quite painless to + # violate the Law of Demeter in violent ways, so you should consider any + # use of `receive_message_chain` a code smell. Even though not all code smells + # indicate real problems (think fluent interfaces), `receive_message_chain` still + # results in brittle examples. For example, if you write + # `foo.receive_message_chain(:bar, :baz => 37)` in a spec and then the + # implementation calls `foo.baz.bar`, the stub will not work. + # + # @example + # + # allow(double).to receive_message_chain("foo.bar") { :baz } + # allow(double).to receive_message_chain(:foo, :bar => :baz) + # allow(double).to receive_message_chain(:foo, :bar) { :baz } + # + # # Given any of ^^ these three forms ^^: + # double.foo.bar # => :baz + # + # # Common use in Rails/ActiveRecord: + # allow(Article).to receive_message_chain("recent.published") { [Article.new] } + # + # @note This is only available when you have enabled the `expect` syntax. end end end diff --git a/lib/rspec/mocks/targets.rb b/lib/rspec/mocks/targets.rb index 9c949d457..00c75309c 100644 --- a/lib/rspec/mocks/targets.rb +++ b/lib/rspec/mocks/targets.rb @@ -10,7 +10,7 @@ def initialize(target) def self.delegate_to(matcher_method) define_method(:to) do |matcher, &block| - unless Matchers::Receive === matcher || Matchers::ReceiveMessages === matcher + unless matcher_allowed?(matcher) raise_unsupported_matcher(:to, matcher) end define_matcher(matcher, matcher_method, &block) @@ -21,8 +21,10 @@ 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) + when Matchers::Receive + define_matcher(matcher, matcher_method, &block) + when Matchers::ReceiveMessages, Matchers::ReceiveMessageChain + raise_negation_unsupported(method_name, matcher) else raise_unsupported_matcher(method_name, matcher) end @@ -37,6 +39,17 @@ def self.disallow_negation(method_name) private + def matcher_allowed?(matcher) + ALLOWED_MATCHERS.include?(matcher.class) + end + + #@api private + ALLOWED_MATCHERS = [ + Matchers::Receive, + Matchers::ReceiveMessages, + Matchers::ReceiveMessageChain, + ] + def define_matcher(matcher, name, &block) matcher.__send__(name, @target, &block) end @@ -87,4 +100,3 @@ class AnyInstanceExpectationTarget < TargetBase end end end - diff --git a/spec/rspec/mocks/and_call_original_spec.rb b/spec/rspec/mocks/and_call_original_spec.rb index 073fc96f9..a1524df5a 100644 --- a/spec/rspec/mocks/and_call_original_spec.rb +++ b/spec/rspec/mocks/and_call_original_spec.rb @@ -2,7 +2,7 @@ require 'delegate' describe "and_call_original" do - context "on a partial mock object" do + context "on a partial double" do let(:klass) do Class.new do def meth_1 @@ -200,7 +200,7 @@ def method_missing(name, *args) end end - context "on a partial mock object that overrides #method" do + context "on a partial double that overrides #method" do let(:request_klass) do Struct.new(:method, :url) do def perform @@ -230,17 +230,17 @@ def request.perform end end - context "on a pure mock object" do + context "on a pure test double" do let(:instance) { double } - it 'raises an error even if the mock object responds to the message' do + it 'raises an error even if the double object responds to the message' do expect(instance.to_s).to be_a(String) mock_expectation = instance.should_receive(:to_s) instance.to_s # to satisfy the expectation expect { mock_expectation.and_call_original - }.to raise_error(/and_call_original.*partial mock/i) + }.to raise_error(/pure test double.*and_call_original.*partial double/i) end end end diff --git a/spec/rspec/mocks/matchers/receive_message_chain_spec.rb b/spec/rspec/mocks/matchers/receive_message_chain_spec.rb new file mode 100644 index 000000000..fb17eb781 --- /dev/null +++ b/spec/rspec/mocks/matchers/receive_message_chain_spec.rb @@ -0,0 +1,188 @@ +require "spec_helper" + + +module RSpec::Mocks::Matchers + describe "receive_message_chain" do + let(:object) { double(:object) } + + context "with only the expect syntax enabled" do + include_context "with syntax", :expect + + it "errors with a negative allowance" do + expect { + allow(object).not_to receive_message_chain(:to_a) + }.to raise_error(RSpec::Mocks::NegationUnsupportedError) + end + + it "errors with a negative expectation" do + expect { + expect(object).not_to receive_message_chain(:to_a) + }.to raise_error(RSpec::Mocks::NegationUnsupportedError) + end + + it "errors with a negative any_instance expectation" do + expect { + expect_any_instance_of(Object).not_to receive_message_chain(:to_a) + }.to raise_error(RSpec::Mocks::NegationUnsupportedError) + end + + it "errors with a negative any_instance allowance" do + expect { + allow_any_instance_of(Object).not_to receive_message_chain(:to_a) + }.to raise_error(RSpec::Mocks::NegationUnsupportedError) + end + + it "works with a do block" do + allow(object).to receive_message_chain(:to_a, :length) do + 3 + end + + expect(object.to_a.length).to eq(3) + end + + it "works with a {} block" do + allow(object).to receive_message_chain(:to_a, :length) { 3 } + + expect(object.to_a.length).to eq(3) + end + + it "gives the { } block prescedence over the do block" do + allow(object).to receive_message_chain(:to_a, :length) { 3 } do + 4 + end + + expect(object.to_a.length).to eq(3) + end + + it "works with and_return" do + allow(object).to receive_message_chain(:to_a, :length).and_return(3) + + expect(object.to_a.length).to eq(3) + end + + it "works with and_call_original", :pending => "See https://github.com/rspec/rspec-mocks/pull/467#issuecomment-28631621" do + list = [1, 2, 3] + expect(list).to receive_message_chain(:to_a, :length).and_call_original + expect(list.to_a.length).to eq(3) + end + + it "fails with and_call_original when the entire chain is not called", :pending => "See https://github.com/rspec/rspec-mocks/pull/467#issuecomment-28631621" do + list = [1, 2, 3] + expect(list).to receive_message_chain(:to_a, :length).and_call_original + expect(list.to_a).to eq([1, 2, 3]) + end + + it "works with and_raise" do + allow(object).to receive_message_chain(:to_a, :length).and_raise(StandardError.new("hi")) + + expect { object.to_a.length }.to raise_error(StandardError, "hi") + end + + it "works with and_throw" do + allow(object).to receive_message_chain(:to_a, :length).and_throw(:nope) + + expect { object.to_a.length }.to throw_symbol(:nope) + end + + it "works with and_yield" do + allow(object).to receive_message_chain(:to_a, :length).and_yield(3) + + expect { |blk| object.to_a.length(&blk) }.to yield_with_args(3) + end + + it "works with a string of messages to chain" do + allow(object).to receive_message_chain("to_a.length").and_yield(3) + + expect { |blk| object.to_a.length(&blk) }.to yield_with_args(3) + end + + it "works with a hash return as the last argument in the chain" do + allow(object).to receive_message_chain(:to_a, :length => 3) + + expect(object.to_a.length).to eq(3) + end + + it "raises when expect is used and the entire chain isn't called" do + expect { + expect(object).to receive_message_chain(:to_a, :farce, :length => 3) + object.to_a + verify_all + }.to raise_error(RSpec::Mocks::MockExpectationError) + end + + it "does not raise when expect is used and the entire chain is called" do + expect { + expect(object).to receive_message_chain(:to_a, :length => 3) + object.to_a.length + verify_all + }.not_to raise_error + end + + it "works with allow_any_instance" do + o = Object.new + + allow_any_instance_of(Object).to receive_message_chain(:to_a, :length => 3) + + expect(o.to_a.length).to eq(3) + end + + it "fails when with expect_any_instance_of is used and the entire chain is not called" do + o = Object.new + + expect { + expect_any_instance_of(Object).to receive_message_chain(:to_a, :length => 3) + verify_all + }.to raise_error(RSpec::Mocks::MockExpectationError) + end + + it "passes when with expect_any_instance_of is used and the entire chain is called" do + o = Object.new + + expect_any_instance_of(Object).to receive_message_chain(:to_a, :length => 3) + o.to_a.length + end + + it "works with expect where the first level of the chain is already expected" do + o = Object.new + expect(o).to receive(:foo).and_return(double) + expect(o).to receive_message_chain(:foo, :bar, :baz) + + o.foo.bar.baz + end + + it "works with allow where the first level of the chain is already expected" do + o = Object.new + expect(o).to receive(:foo).and_return(double) + allow(o).to receive_message_chain(:foo, :bar, :baz).and_return(3) + + expect(o.foo.bar.baz).to eq(3) + end + + it "works with expect where the first level of the chain is already stubbed" do + o = Object.new + allow(o).to receive(:foo).and_return(double) + expect(o).to receive_message_chain(:foo, :bar, :baz) + + o.foo.bar.baz + end + + it "works with allow where the first level of the chain is already stubbed" do + o = Object.new + allow(o).to receive(:foo).and_return(double) + allow(o).to receive_message_chain(:foo, :bar, :baz).and_return(3) + + expect(o.foo.bar.baz).to eq(3) + end + end + + context "when the expect and should syntaxes are enabled" do + include_context "with syntax", [:expect, :should] + + it "stubs the message correctly" do + allow(object).to receive_message_chain(:to_a, :length) + + expect { object.to_a.length }.not_to raise_error + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fcb4c4fe..9a40070db 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,6 +27,10 @@ def verify(object) def reset(object) RSpec::Mocks.proxy_for(object).reset end + + def verify_all + RSpec::Mocks.space.verify_all + end end module DeprecationHelpers