From 069391f7f0e5af75a9961089ca20da1fd610f556 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Sat, 15 Jun 2024 21:55:01 +0100 Subject: [PATCH] Merge pull request #1455 from nevinera/nev--1132--handle-dynamic-matches-on-mocks-specially Handle dynamic mocks specially for null-object doubles --- lib/rspec/matchers/built_in/has.rb | 29 ++++++++++++++++++++++-- spec/rspec/matchers/built_in/be_spec.rb | 14 ++++++++++++ spec/rspec/matchers/built_in/has_spec.rb | 14 ++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/rspec/matchers/built_in/has.rb b/lib/rspec/matchers/built_in/has.rb index 5036a6405..ccd4d42f4 100644 --- a/lib/rspec/matchers/built_in/has.rb +++ b/lib/rspec/matchers/built_in/has.rb @@ -46,8 +46,25 @@ def description private + # Catch a semi-frequent typo - if you have strict_predicate_matchers disabled and + # expect(spy).to have_receieveddd(:foo) it would be evergreen - the dynamic matcher + # queries `has_receiveddd?`, the spy _fakes_ the method, returning its (truthy) self. + if defined?(RSpec::Mocks::Double) + def really_responds_to?(method) + if RSpec::Mocks::Double === @actual + @actual.respond_to?(method) && methods_include?(method) + else + @actual.respond_to?(method) + end + end + else + def really_responds_to?(method) + @actual.respond_to?(method) + end + end + def predicate_accessible? - @actual.respond_to? predicate + really_responds_to?(predicate) end # support 1.8.7, evaluate once at load time for performance @@ -56,11 +73,19 @@ def predicate_accessible? def private_predicate? @actual.private_methods.include? predicate.to_s end + + def methods_include?(method) + @actual.methods.include?(method.to_s) + end # :nocov: else def private_predicate? @actual.private_methods.include? predicate end + + def methods_include?(method) + @actual.methods.include?(method) + end end def predicate_result @@ -155,7 +180,7 @@ def failure_to_respond_explanation end def predicate_accessible? - super || actual.respond_to?(present_tense_predicate) + super || really_responds_to?(present_tense_predicate) end def present_tense_predicate diff --git a/spec/rspec/matchers/built_in/be_spec.rb b/spec/rspec/matchers/built_in/be_spec.rb index 42ba25ff4..b4d3aff9d 100644 --- a/spec/rspec/matchers/built_in/be_spec.rb +++ b/spec/rspec/matchers/built_in/be_spec.rb @@ -85,6 +85,20 @@ expect(actual).to be_happy }.to fail_with("expected `#{actual.inspect}.happy?` to be truthy, got false") end + + it "allows dynamic matchers to pass for supplied methods on spies" do + thing = spy("thing", :has_recv? => true) + expect(thing).to have_recv(:foo) + expect(thing).to have_received(:has_recv?).with(:foo) + end + + it "does not allow dynamic matchers to pass for inferred methods on spies" do + thing = spy("thing") + expect { + expect(thing).to have_recv(:foo) + }.to fail + expect(thing).not_to have_received(:has_recv?) + end end it "fails when actual does not respond to :predicate?" do diff --git a/spec/rspec/matchers/built_in/has_spec.rb b/spec/rspec/matchers/built_in/has_spec.rb index c392b12fa..2b1709234 100644 --- a/spec/rspec/matchers/built_in/has_spec.rb +++ b/spec/rspec/matchers/built_in/has_spec.rb @@ -159,6 +159,20 @@ def o.has_sym?(sym); sym == :foo; end actual = double("actual", :has_foo? => nil) expect(actual).not_to have_foo end + + it "allows dynamic matchers to pass for supplied methods on spies" do + thing = spy("thing", :furg? => true) + expect(thing).to be_furg(:foo) + expect(thing).to have_received(:furg?).with(:foo) + end + + it "does not allow dynamic matchers to pass for inferred methods on spies" do + thing = spy("thing") + expect { + expect(thing).to be_furg(:foo) + }.to fail + expect(thing).not_to have_received(:furg?) + end end it "fails if #has_sym?(*args) returns true" do