From 66055a6afd0cf09e9d57c1c9971348860cd0d685 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 2 Apr 2014 12:10:11 -0700 Subject: [PATCH 1/4] Refactor: centralize location/expected_from. --- lib/rspec/mocks.rb | 10 ++-------- lib/rspec/mocks/any_instance/expectation_chain.rb | 6 ++++-- lib/rspec/mocks/any_instance/stub_chain.rb | 6 ++++-- lib/rspec/mocks/matchers/receive.rb | 3 +-- lib/rspec/mocks/proxy.rb | 10 ++++++---- lib/rspec/mocks/syntax.rb | 5 +---- lib/rspec/mocks/verifying_proxy.rb | 4 ++-- 7 files changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/rspec/mocks.rb b/lib/rspec/mocks.rb index ca518a0af..17b2831bc 100644 --- a/lib/rspec/mocks.rb +++ b/lib/rspec/mocks.rb @@ -67,10 +67,7 @@ def self.teardown # x = 0 # RSpec::Mocks.allow_message(bar, :foo) { x += 1 } def self.allow_message(subject, message, opts={}, &block) - orig_caller = opts.fetch(:expected_from) { - CallerFilter.first_non_rspec_line - } - space.proxy_for(subject).add_stub(orig_caller, message, opts, &block) + space.proxy_for(subject).add_stub(message, opts, &block) end # Sets a message expectation on `subject`. @@ -85,10 +82,7 @@ def self.allow_message(subject, message, opts={}, &block) # RSpec::Mocks.expect_message(bar, :foo) # bar.foo def self.expect_message(subject, message, opts={}, &block) - orig_caller = opts.fetch(:expected_from) { - CallerFilter.first_non_rspec_line - } - space.proxy_for(subject).add_message_expectation(orig_caller, message, opts, &block) + space.proxy_for(subject).add_message_expectation(message, opts, &block) end # Call the passed block and verify mocks after it has executed. This allows diff --git a/lib/rspec/mocks/any_instance/expectation_chain.rb b/lib/rspec/mocks/any_instance/expectation_chain.rb index ff85dfc98..153f37be1 100644 --- a/lib/rspec/mocks/any_instance/expectation_chain.rb +++ b/lib/rspec/mocks/any_instance/expectation_chain.rb @@ -25,8 +25,10 @@ class PositiveExpectationChain < ExpectationChain def create_message_expectation_on(instance) proxy = ::RSpec::Mocks.space.proxy_for(instance) - expected_from = IGNORED_BACKTRACE_LINE - me = proxy.add_message_expectation(expected_from, *@expectation_args, &@expectation_block) + method_name, opts = @expectation_args + opts = (opts || {}).merge(:expected_form => IGNORED_BACKTRACE_LINE) + + me = proxy.add_message_expectation(method_name, opts, &@expectation_block) if RSpec::Mocks.configuration.yield_receiver_to_any_instance_implementation_blocks? me.and_yield_receiver_to_implementation end diff --git a/lib/rspec/mocks/any_instance/stub_chain.rb b/lib/rspec/mocks/any_instance/stub_chain.rb index e7459c4a1..16071eca9 100644 --- a/lib/rspec/mocks/any_instance/stub_chain.rb +++ b/lib/rspec/mocks/any_instance/stub_chain.rb @@ -13,8 +13,10 @@ def expectation_fulfilled? def create_message_expectation_on(instance) proxy = ::RSpec::Mocks.space.proxy_for(instance) - expected_from = IGNORED_BACKTRACE_LINE - stub = proxy.add_stub(expected_from, *@expectation_args, &@expectation_block) + method_name, opts = @expectation_args + opts = (opts || {}).merge(:expected_form => IGNORED_BACKTRACE_LINE) + + stub = proxy.add_stub(method_name, opts, &@expectation_block) @recorder.stubs[stub.message] << stub if RSpec::Mocks.configuration.yield_receiver_to_any_instance_implementation_blocks? diff --git a/lib/rspec/mocks/matchers/receive.rb b/lib/rspec/mocks/matchers/receive.rb index 9a12fab16..be41e1996 100644 --- a/lib/rspec/mocks/matchers/receive.rb +++ b/lib/rspec/mocks/matchers/receive.rb @@ -9,7 +9,6 @@ def initialize(message, block) @message = message @block = block @recorded_customizations = [] - @backtrace_line = CallerFilter.first_non_rspec_line end def name @@ -74,7 +73,7 @@ def warn_if_any_instance(expression, subject) def setup_mock_proxy_method_substitute(subject, method, block) proxy = ::RSpec::Mocks.space.proxy_for(subject) - setup_method_substitute(proxy, method, block, @backtrace_line) + setup_method_substitute(proxy, method, block) end def setup_any_instance_method_substitute(subject, method, block) diff --git a/lib/rspec/mocks/proxy.rb b/lib/rspec/mocks/proxy.rb index 95bc470cd..c1719b8bc 100644 --- a/lib/rspec/mocks/proxy.rb +++ b/lib/rspec/mocks/proxy.rb @@ -42,7 +42,8 @@ def original_method_handle_for(message) end # @private - def add_message_expectation(location, method_name, opts={}, &block) + def add_message_expectation(method_name, opts={}, &block) + location = opts.fetch(:expected_from) { CallerFilter.first_non_rspec_line } meth_double = method_double_for(method_name) if null_object? && !block @@ -101,7 +102,8 @@ def check_for_unexpected_arguments(expectation) end # @private - def add_stub(location, method_name, opts={}, &implementation) + def add_stub(method_name, opts={}, &implementation) + location = opts.fetch(:expected_from) { CallerFilter.first_non_rspec_line } method_double_for(method_name).add_stub @error_generator, @order_group, location, opts, &implementation end @@ -380,7 +382,7 @@ def initialize(order_group) attr_accessor :warn_about_expectations alias warn_about_expectations? warn_about_expectations - def add_message_expectation(location, method_name, opts={}, &block) + def add_message_expectation(method_name, opts={}, &block) warn(method_name) if warn_about_expectations? super end @@ -390,7 +392,7 @@ def add_negative_message_expectation(location, method_name, &implementation) super end - def add_stub(location, method_name, opts={}, &implementation) + def add_stub(method_name, opts={}, &implementation) warn(method_name) if warn_about_expectations? super end diff --git a/lib/rspec/mocks/syntax.rb b/lib/rspec/mocks/syntax.rb index 0f32d680f..8d780c5eb 100644 --- a/lib/rspec/mocks/syntax.rb +++ b/lib/rspec/mocks/syntax.rb @@ -30,14 +30,12 @@ def self.enable_should(syntax_host = default_should_syntax_host) syntax_host.class_exec do def should_receive(message, opts={}, &block) ::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__) - opts[:expected_from] ||= CallerFilter.first_non_rspec_line ::RSpec::Mocks.expect_message(self, message, opts, &block) end def should_not_receive(message, &block) ::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__) - opts = {:expected_from => CallerFilter.first_non_rspec_line} - ::RSpec::Mocks.expect_message(self, message, opts, &block).never + ::RSpec::Mocks.expect_message(self, message, {}, &block).never end def stub(message_or_hash, opts={}, &block) @@ -45,7 +43,6 @@ def stub(message_or_hash, opts={}, &block) if ::Hash === message_or_hash message_or_hash.each {|message, value| stub(message).and_return value } else - opts[:expected_from] = CallerFilter.first_non_rspec_line ::RSpec::Mocks.allow_message(self, message_or_hash, opts, &block) end end diff --git a/lib/rspec/mocks/verifying_proxy.rb b/lib/rspec/mocks/verifying_proxy.rb index d9dce85d5..53e86c0ff 100644 --- a/lib/rspec/mocks/verifying_proxy.rb +++ b/lib/rspec/mocks/verifying_proxy.rb @@ -6,7 +6,7 @@ module Mocks # @private module VerifyingProxyMethods - def add_stub(location, method_name, opts={}, &implementation) + def add_stub(method_name, opts={}, &implementation) ensure_implemented(method_name) super end @@ -16,7 +16,7 @@ def add_simple_stub(method_name, *args) super end - def add_message_expectation(location, method_name, opts={}, &block) + def add_message_expectation(method_name, opts={}, &block) ensure_implemented(method_name) super end From 37447c351a614e102b9bb78cc81b6a44e367a81f Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 2 Apr 2014 16:19:03 -0700 Subject: [PATCH 2/4] Add failing spec. --- spec/rspec/mocks/any_instance_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/rspec/mocks/any_instance_spec.rb b/spec/rspec/mocks/any_instance_spec.rb index 27f0d31e0..05506bbb8 100644 --- a/spec/rspec/mocks/any_instance_spec.rb +++ b/spec/rspec/mocks/any_instance_spec.rb @@ -167,6 +167,15 @@ def private_method; :private_method_return_value; end expect(klass.new.foo).to be(return_value) expect(klass.new.foo).to be(return_value) end + + it "can change how instances responds in the middle of an example" do + instance = klass.new + + allow_any_instance_of(klass).to receive(:foo).and_return(1) + expect(instance.foo).to eq(1) + allow_any_instance_of(klass).to receive(:foo).and_return(2) + expect(instance.foo).to eq(2) + end end context "with #and_yield" do From e00d6d7cf0f044bc9bd55d1defbbb433a88d67a4 Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 2 Apr 2014 16:26:33 -0700 Subject: [PATCH 3/4] Make `any_instance` update existing instance stubs. Fixes #613. --- Changelog.md | 2 + lib/rspec/mocks/any_instance.rb | 1 + lib/rspec/mocks/any_instance/proxy.rb | 102 ++++++++++++++++++ lib/rspec/mocks/any_instance/recorder.rb | 15 +-- lib/rspec/mocks/matchers/receive.rb | 6 +- .../mocks/matchers/receive_message_chain.rb | 8 +- lib/rspec/mocks/matchers/receive_messages.rb | 2 +- lib/rspec/mocks/method_double.rb | 7 +- lib/rspec/mocks/proxy.rb | 4 +- lib/rspec/mocks/space.rb | 8 ++ lib/rspec/mocks/syntax.rb | 2 +- spec/rspec/mocks/any_instance_spec.rb | 29 ++++- .../matchers/receive_message_chain_spec.rb | 20 ++++ .../mocks/matchers/receive_messages_spec.rb | 9 ++ spec/rspec/mocks/matchers/receive_spec.rb | 10 +- spec/rspec/mocks/should_syntax_spec.rb | 38 ++++++- 16 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 lib/rspec/mocks/any_instance/proxy.rb diff --git a/Changelog.md b/Changelog.md index 4f0127ae3..e3e845e31 100644 --- a/Changelog.md +++ b/Changelog.md @@ -37,6 +37,8 @@ Bug Fixes: on an any instance. (Xavier Shay) * Fix `and_call_original` to work properly when multiple classes in an inheritance hierarchy have been stubbed with the same method. (Myron Marston) +* Fix `any_instance` so that it updates existing instances that have + already been stubbed. (Myron Marston) ### 3.0.0.beta2 / 2014-02-17 [Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.0.0.beta1...v3.0.0.beta2) diff --git a/lib/rspec/mocks/any_instance.rb b/lib/rspec/mocks/any_instance.rb index 1cae54ae6..bc912d84b 100644 --- a/lib/rspec/mocks/any_instance.rb +++ b/lib/rspec/mocks/any_instance.rb @@ -6,4 +6,5 @@ any_instance/expectation_chain any_instance/message_chains any_instance/recorder + any_instance/proxy ].each { |f| RSpec::Support.require_rspec_mocks(f) } diff --git a/lib/rspec/mocks/any_instance/proxy.rb b/lib/rspec/mocks/any_instance/proxy.rb new file mode 100644 index 000000000..c6d4eabe5 --- /dev/null +++ b/lib/rspec/mocks/any_instance/proxy.rb @@ -0,0 +1,102 @@ +module RSpec + module Mocks + module AnyInstance + # @private + # The `AnyInstance::Recorder` is responsible for redefining the klass's + # instance method in order to add any stubs/expectations the first time + # the method is called. It's not capable of updating a stub on an instance + # that's already been previously stubbed (either directly, or via + # `any_instance`). + # + # This proxy sits in front of the recorder and delegates both to it + # and to the `RSpec::Mocks::Proxy` for each already mocked or stubbed + # instance of the class, in order to propogates changes to the instances. + # + # Note that unlike `RSpec::Mocks::Proxy`, this proxy class is stateless + # and is not persisted in `RSpec::Mocks.space`. + # + # Proxying for the message expectation fluent interface (typically chained + # off of the return value of one of these methods) is provided by the + # `FluentInterfaceProxy` class below. + class Proxy + def initialize(recorder, target_proxies) + @recorder = recorder + @target_proxies = target_proxies + end + + def klass + @recorder.klass + end + + def stub(method_name_or_method_map, &block) + if Hash === method_name_or_method_map + method_name_or_method_map.each do |method_name, return_value| + stub(method_name).and_return(return_value) + end + else + perform_proxying(__method__, [method_name_or_method_map], block) do |proxy| + proxy.add_stub(method_name_or_method_map, &block) + end + end + end + + def unstub(method_name) + perform_proxying(__method__, [method_name], nil) do |proxy| + proxy.remove_stub_if_present(method_name) + end + end + + def stub_chain(*chain, &block) + perform_proxying(__method__, chain, block) do |proxy| + Mocks::StubChain.stub_chain_on(proxy.object, *chain, &block) + end + end + + def expect_chain(*chain, &block) + perform_proxying(__method__, chain, block) do |proxy| + Mocks::ExpectChain.expect_chain_on(proxy.object, *chain, &block) + end + end + + def should_receive(method_name, &block) + perform_proxying(__method__, [method_name], block) do |proxy| + proxy.add_message_expectation(method_name, &block) + end + end + + def should_not_receive(method_name, &block) + perform_proxying(__method__, [method_name], block) do |proxy| + proxy.add_message_expectation(method_name, &block).never + end + end + + private + + def perform_proxying(method_name, args, block, &target_proxy_block) + recorder_value = @recorder.__send__(method_name, *args, &block) + proxy_values = @target_proxies.map(&target_proxy_block) + FluentInterfaceProxy.new([recorder_value] + proxy_values) + end + end + + # @private + # Delegates messages to each of the given targets in order to + # provide the fluent interface that is available off of message + # expectations when dealing with `any_instance`. + # + # `targets` will typically contain 1 of the `AnyInstance::Recorder` + # return values and N `MessageExpectation` instances (one per instance + # of the `any_instance` klass). + class FluentInterfaceProxy + def initialize(targets) + @targets = targets + end + + def method_missing(*args, &block) + return_values = @targets.map { |t| t.__send__(*args, &block) } + FluentInterfaceProxy.new(return_values) + end + end + end + end +end diff --git a/lib/rspec/mocks/any_instance/recorder.rb b/lib/rspec/mocks/any_instance/recorder.rb index b43ea2242..69f773280 100644 --- a/lib/rspec/mocks/any_instance/recorder.rb +++ b/lib/rspec/mocks/any_instance/recorder.rb @@ -26,15 +26,9 @@ def initialize(klass) # instance of this object that invokes the submitted method. # # @see Methods#stub - def stub(method_name_or_method_map, &block) - if Hash === method_name_or_method_map - method_name_or_method_map.each do |method_name, return_value| - stub(method_name).and_return(return_value) - end - else - observe!(method_name_or_method_map) - message_chains.add(method_name_or_method_map, StubChain.new(self, method_name_or_method_map, &block)) - end + def stub(method_name, &block) + observe!(method_name) + message_chains.add(method_name, StubChain.new(self, method_name, &block)) end # Initializes the recording a stub chain to be played back against any @@ -85,9 +79,6 @@ def unstub(method_name) raise RSpec::Mocks::MockExpectationError, "The method `#{method_name}` was not stubbed or was already unstubbed" end message_chains.remove_stub_chains_for!(method_name) - ::RSpec::Mocks.space.proxies_of(@klass).each do |proxy| - stubs[method_name].each { |stub| proxy.remove_single_stub(method_name, stub) } - end stubs[method_name].clear stop_observing!(method_name) unless message_chains.has_expectation?(method_name) end diff --git a/lib/rspec/mocks/matchers/receive.rb b/lib/rspec/mocks/matchers/receive.rb index be41e1996..e5ff3941f 100644 --- a/lib/rspec/mocks/matchers/receive.rb +++ b/lib/rspec/mocks/matchers/receive.rb @@ -61,7 +61,7 @@ def setup_any_instance_allowance(subject, &block) private def warn_if_any_instance(expression, subject) - if AnyInstance::Recorder === subject + if AnyInstance::Proxy === subject RSpec.warning( "`#{expression}(#{subject.klass}.any_instance).to` " << "is probably not what you meant, it does not operate on " << @@ -77,8 +77,8 @@ def setup_mock_proxy_method_substitute(subject, method, block) end def setup_any_instance_method_substitute(subject, method, block) - any_instance_recorder = ::RSpec::Mocks.space.any_instance_recorder_for(subject) - setup_method_substitute(any_instance_recorder, method, block) + proxy = ::RSpec::Mocks.space.any_instance_proxy_for(subject) + setup_method_substitute(proxy, method, block) end def setup_method_substitute(host, method, block, *args) diff --git a/lib/rspec/mocks/matchers/receive_message_chain.rb b/lib/rspec/mocks/matchers/receive_message_chain.rb index 51930371f..39365ee4c 100644 --- a/lib/rspec/mocks/matchers/receive_message_chain.rb +++ b/lib/rspec/mocks/matchers/receive_message_chain.rb @@ -28,14 +28,14 @@ def setup_allowance(subject, &block) end def setup_any_instance_allowance(subject, &block) - recorder = ::RSpec::Mocks.space.any_instance_recorder_for(subject) - chain = recorder.stub_chain(*@chain, &(@block || block)) + proxy = ::RSpec::Mocks.space.any_instance_proxy_for(subject) + chain = proxy.stub_chain(*@chain, &(@block || block)) replay_customizations(chain) end def setup_any_instance_expectation(subject, &block) - recorder = ::RSpec::Mocks.space.any_instance_recorder_for(subject) - chain = recorder.expect_chain(*@chain, &(@block || block)) + proxy = ::RSpec::Mocks.space.any_instance_proxy_for(subject) + chain = proxy.expect_chain(*@chain, &(@block || block)) replay_customizations(chain) end diff --git a/lib/rspec/mocks/matchers/receive_messages.rb b/lib/rspec/mocks/matchers/receive_messages.rb index 52c7f9bc6..1fcb03d04 100644 --- a/lib/rspec/mocks/matchers/receive_messages.rb +++ b/lib/rspec/mocks/matchers/receive_messages.rb @@ -58,7 +58,7 @@ def proxy_on(subject) end def any_instance_of(subject) - ::RSpec::Mocks.space.any_instance_recorder_for(subject) + ::RSpec::Mocks.space.any_instance_proxy_for(subject) end def each_message_on(host) diff --git a/lib/rspec/mocks/method_double.rb b/lib/rspec/mocks/method_double.rb index 45b7d47dd..785423c4b 100644 --- a/lib/rspec/mocks/method_double.rb +++ b/lib/rspec/mocks/method_double.rb @@ -189,13 +189,12 @@ def add_default_stub(*args, &implementation) # @private def remove_stub raise_method_not_stubbed_error if stubs.empty? - expectations.empty? ? reset : stubs.clear + remove_stub_if_present end # @private - def remove_single_stub(stub) - stubs.delete(stub) - restore_original_method if stubs.empty? && expectations.empty? + def remove_stub_if_present + expectations.empty? ? reset : stubs.clear end # @private diff --git a/lib/rspec/mocks/proxy.rb b/lib/rspec/mocks/proxy.rb index c1719b8bc..8c2a0abfd 100644 --- a/lib/rspec/mocks/proxy.rb +++ b/lib/rspec/mocks/proxy.rb @@ -118,8 +118,8 @@ def remove_stub(method_name) end # @private - def remove_single_stub(method_name, stub) - method_double_for(method_name).remove_single_stub(stub) + def remove_stub_if_present(method_name) + method_double_for(method_name).remove_stub_if_present end # @private diff --git a/lib/rspec/mocks/space.rb b/lib/rspec/mocks/space.rb index 9aec66e9e..d8a1bbc49 100644 --- a/lib/rspec/mocks/space.rb +++ b/lib/rspec/mocks/space.rb @@ -13,6 +13,10 @@ def any_instance_recorder_for(*args) raise_lifecycle_message end + def any_instance_proxy_for(*args) + raise_lifecycle_message + end + def register_constant_mutator(mutator) raise_lifecycle_message end @@ -85,6 +89,10 @@ def any_instance_recorder_for(klass) end end + def any_instance_proxy_for(klass) + AnyInstance::Proxy.new(any_instance_recorder_for(klass), proxies_of(klass)) + end + def proxies_of(klass) proxies.values.select { |proxy| klass === proxy.object } end diff --git a/lib/rspec/mocks/syntax.rb b/lib/rspec/mocks/syntax.rb index 8d780c5eb..ebd20994e 100644 --- a/lib/rspec/mocks/syntax.rb +++ b/lib/rspec/mocks/syntax.rb @@ -77,7 +77,7 @@ def received_message?(message, *args, &block) Class.class_exec do def any_instance ::RSpec::Mocks::Syntax.warn_unless_should_configured(__method__) - ::RSpec::Mocks.space.any_instance_recorder_for(self) + ::RSpec::Mocks.space.any_instance_proxy_for(self) end end end diff --git a/spec/rspec/mocks/any_instance_spec.rb b/spec/rspec/mocks/any_instance_spec.rb index 05506bbb8..8b691ec6b 100644 --- a/spec/rspec/mocks/any_instance_spec.rb +++ b/spec/rspec/mocks/any_instance_spec.rb @@ -175,6 +175,8 @@ def private_method; :private_method_return_value; end expect(instance.foo).to eq(1) allow_any_instance_of(klass).to receive(:foo).and_return(2) expect(instance.foo).to eq(2) + allow_any_instance_of(klass).to receive(:foo).and_raise("boom") + expect { instance.foo }.to raise_error("boom") end end @@ -308,7 +310,6 @@ class RSpec::SampleRspecTestClass;end obj.existing_method allow_any_instance_of(klass).to receive(:existing_method).and_call_original - pending "not working for `and_call_original` yet, but works with `unstub`" expect(obj.existing_method).to eq(:existing_method_return_value) end @@ -318,16 +319,15 @@ class RSpec::SampleRspecTestClass;end expect(obj.existing_method).to eq(:any_instance_value) allow_any_instance_of(klass).to receive(:existing_method).and_call_original - pending "not working for `and_call_original` yet, but works with `unstub`" expect(obj.existing_method).to eq(:existing_method_return_value) end - it "does not remove any stubs set directly on an instance" do + it "removes any stubs set directly on an instance" do allow_any_instance_of(klass).to receive(:existing_method).and_return(:any_instance_value) obj = klass.new allow(obj).to receive(:existing_method).and_return(:local_method) allow_any_instance_of(klass).to receive(:existing_method).and_call_original - expect(obj.existing_method).to eq(:local_method) + expect(obj.existing_method).to eq(:existing_method_return_value) end it "does not remove any expectations with the same method name" do @@ -360,6 +360,15 @@ class RSpec::SampleRspecTestClass;end expect { klass.new.another_existing_method }.to_not raise_error end + it "affects previously stubbed instances" do + instance = klass.new + + allow_any_instance_of(klass).to receive(:foo).and_return(1) + expect(instance.foo).to eq(1) + expect_any_instance_of(klass).not_to receive(:foo) + expect { instance.foo }.to fail + end + context "with constraints" do it "fails if the method is called with the specified parameters" do expect_any_instance_of(klass).not_to receive(:existing_method_with_arguments).with(:argument_one, :argument_two) @@ -409,6 +418,18 @@ def inspect reset_all end + it "affects previously stubbed instances" do + instance = klass.new + + allow_any_instance_of(klass).to receive(:foo).and_return(1) + expect(instance.foo).to eq(1) + expect_any_instance_of(klass).to receive(:foo).with(2).and_return(2) + expect(instance.foo(2)).to eq(2) + + # TODO: this shouldn't be necessary to satisfy the expectation, but is. + klass.new.foo(2) + end + context "with an expectation is set on a method which does not exist" do it "returns the expected value" do expect_any_instance_of(klass).to receive(:foo).and_return(1) diff --git a/spec/rspec/mocks/matchers/receive_message_chain_spec.rb b/spec/rspec/mocks/matchers/receive_message_chain_spec.rb index bac1ef88a..3931ac62d 100644 --- a/spec/rspec/mocks/matchers/receive_message_chain_spec.rb +++ b/spec/rspec/mocks/matchers/receive_message_chain_spec.rb @@ -143,6 +143,15 @@ module RSpec::Mocks::Matchers expect(o.to_a.length).to eq(3) end + it "stubs already stubbed instances when using `allow_any_instance_of`" do + o = Object.new + allow(o).to receive(:foo).and_return(dbl = double) + expect(o.foo).to be(dbl) + + allow_any_instance_of(Object).to receive_message_chain(:foo, :bar).and_return("bazz") + expect(o.foo.bar).to eq("bazz") + end + it "fails when with expect_any_instance_of is used and the entire chain is not called" do expect { expect_any_instance_of(Object).to receive_message_chain(:to_a, :length => 3) @@ -150,6 +159,17 @@ module RSpec::Mocks::Matchers }.to raise_error(RSpec::Mocks::MockExpectationError) end + it "affects previously stubbed instances when `expect_any_instance_of` is called" do + o = Object.new + allow(o).to receive(:foo).and_return(double) + + expect_any_instance_of(Object).to receive_message_chain(:foo, :bar => 3) + expect(o.foo.bar).to eq(3) + + # TODO: this shouldn't be necessary to satisfy the expectation, but is. + Object.new.foo.bar + end + it "passes when with expect_any_instance_of is used and the entire chain is called" do o = Object.new diff --git a/spec/rspec/mocks/matchers/receive_messages_spec.rb b/spec/rspec/mocks/matchers/receive_messages_spec.rb index 28484cc52..4155aab89 100644 --- a/spec/rspec/mocks/matchers/receive_messages_spec.rb +++ b/spec/rspec/mocks/matchers/receive_messages_spec.rb @@ -52,6 +52,15 @@ module Mocks expect(obj.b).to eq 2 end + it "updates stubs on instances with existing stubs" do + allow(obj).to receive(:a).and_return(3) + expect(obj.a).to eq(3) + + 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 diff --git a/spec/rspec/mocks/matchers/receive_spec.rb b/spec/rspec/mocks/matchers/receive_spec.rb index 6cc552d10..c36e120f5 100644 --- a/spec/rspec/mocks/matchers/receive_spec.rb +++ b/spec/rspec/mocks/matchers/receive_spec.rb @@ -18,14 +18,16 @@ module Mocks it "warns about expect(Klass.any_instance).to receive..." do expect(RSpec).to receive(:warning).with(/expect.*any_instance.*is probably not what you meant.*expect_any_instance_of.*instead/) - expect(Object.any_instance).to receive(:foo) - Object.any_instance.foo + any_instance_proxy = Object.any_instance + expect(any_instance_proxy).to receive(:foo) + any_instance_proxy.foo end it "includes the correct call site in the expect warning" do + any_instance_proxy = Object.any_instance expect_warning_with_call_site(__FILE__, __LINE__ + 1) - expect(Object.any_instance).to receive(:foo) - Object.any_instance.foo + expect(any_instance_proxy).to receive(:foo) + any_instance_proxy.foo end end diff --git a/spec/rspec/mocks/should_syntax_spec.rb b/spec/rspec/mocks/should_syntax_spec.rb index ed9f6e165..59738a962 100644 --- a/spec/rspec/mocks/should_syntax_spec.rb +++ b/spec/rspec/mocks/should_syntax_spec.rb @@ -244,6 +244,25 @@ def use_rspec_mocks expect { verify_all }.to raise_error(RSpec::Mocks::MockExpectationError) end + it 'affects previously stubbed instances when stubbing a method' do + instance = klass.new + klass.any_instance.stub(:foo).and_return(2) + expect(instance.foo).to eq(2) + klass.any_instance.stub(:foo).and_return(1) + expect(instance.foo).to eq(1) + end + + it 'affects previously stubbed instances when mocking a method' do + instance = klass.new + klass.any_instance.stub(:foo).and_return(2) + expect(instance.foo).to eq(2) + klass.any_instance.should_receive(:foo).and_return(1) + expect(instance.foo).to eq(1) + + # TODO: this shouldn't be necessary to satisfy the expectation, but is. + klass.new.foo(2) + end + context "invocation order" do describe "#stub" do it "raises an error if 'stub' follows 'with'" do @@ -296,6 +315,15 @@ def use_rspec_mocks expect(klass.new.one.two.three).to eq(:four) end end + + it 'affects previously stubbed instances' do + instance = klass.new + dbl = double + klass.any_instance.stub(:foo).and_return(dbl) + expect(instance.foo).to eq(dbl) + klass.any_instance.stub_chain(:foo, :bar => 3) + expect(instance.foo.bar).to eq(3) + end end describe "#should_receive" do @@ -379,11 +407,19 @@ def use_rspec_mocks expect(obj.existing_method).to eq(:existing_method_return_value) end - it "does not remove any stubs set directly on an instance" do + it "removes stubs set directly on an instance" do klass.any_instance.stub(:existing_method).and_return(:any_instance_value) obj = klass.new obj.stub(:existing_method).and_return(:local_method) klass.any_instance.unstub(:existing_method) + expect(obj.existing_method).to eq(:existing_method_return_value) + end + + it "does not remove message expectations set directly on an instance" do + klass.any_instance.stub(:existing_method).and_return(:any_instance_value) + obj = klass.new + obj.should_receive(:existing_method).and_return(:local_method) + klass.any_instance.unstub(:existing_method) expect(obj.existing_method).to eq(:local_method) end From 62ad32162af34f096b679f6b53c2ab9001e1ce8c Mon Sep 17 00:00:00 2001 From: Myron Marston Date: Wed, 2 Apr 2014 23:46:31 -0700 Subject: [PATCH 4/4] Implement `respond_to?` properly. --- lib/rspec/mocks/any_instance/proxy.rb | 10 ++++++++++ spec/rspec/mocks/should_syntax_spec.rb | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/lib/rspec/mocks/any_instance/proxy.rb b/lib/rspec/mocks/any_instance/proxy.rb index c6d4eabe5..20c4519b7 100644 --- a/lib/rspec/mocks/any_instance/proxy.rb +++ b/lib/rspec/mocks/any_instance/proxy.rb @@ -92,6 +92,16 @@ def initialize(targets) @targets = targets end + if RUBY_VERSION.to_f > 1.8 + def respond_to_missing?(method_name, include_private = false) + super || @targets.first.respond_to?(method_name, include_private) + end + else + def respond_to?(method_name, include_private = false) + super || @targets.first.respond_to?(method_name, include_private) + end + end + def method_missing(*args, &block) return_values = @targets.map { |t| t.__send__(*args, &block) } FluentInterfaceProxy.new(return_values) diff --git a/spec/rspec/mocks/should_syntax_spec.rb b/spec/rspec/mocks/should_syntax_spec.rb index 59738a962..d9aa882c1 100644 --- a/spec/rspec/mocks/should_syntax_spec.rb +++ b/spec/rspec/mocks/should_syntax_spec.rb @@ -244,6 +244,13 @@ def use_rspec_mocks expect { verify_all }.to raise_error(RSpec::Mocks::MockExpectationError) end + it 'can get method objects for the fluent interface', :if => RUBY_VERSION.to_f > 1.8 do + and_return = klass.any_instance.stub(:foo).method(:and_return) + and_return.call(23) + + expect(klass.new.foo).to eq(23) + end + it 'affects previously stubbed instances when stubbing a method' do instance = klass.new klass.any_instance.stub(:foo).and_return(2)