From e931e818b577172b89fb4583fc336fbcd25df36b Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 18 Feb 2022 11:19:47 +0100 Subject: [PATCH] Display keyword hashes in in expectation error messages Ref: https://github.com/vcr/vcr/pull/925 Ref: https://github.com/rspec/rspec-mocks/pull/1394 I spent quite a lot of time figuring this error: ``` 2) VCR.turned_on passes options through to .turn_off! Failure/Error: turn_off!(options) VCR received :turn_off! with unexpected arguments expected: ({:ignore_cassettes=>true}) got: ({:ignore_cassettes=>true}) # ./lib/vcr.rb:317:in `turned_on' # ./spec/lib/vcr_spec.rb:367:in `block (3 levels) in ' ``` I quickly suspected it was a keyword argument issue, but it's far from obvious to everyone, and even when you are familair with the issue it doesn't tell you what was expected and what was received. I doubt the way I implemented this is ok, but I think it's worth opening the discussion ``` 2) VCR.turned_on passes options through to .turn_off! Failure/Error: turn_off!(options) VCR received :turn_off! with unexpected arguments expected: ({:ignore_cassettes=>true}) (keyword arguments) got: ({:ignore_cassettes=>true}) (options hash) # ./lib/vcr.rb:317:in `turned_on' # ./spec/lib/vcr_spec.rb:367:in `block (3 levels) in ' ``` --- lib/rspec/mocks/error_generator.rb | 11 ++++++++++ spec/rspec/mocks/diffing_spec.rb | 33 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/rspec/mocks/error_generator.rb b/lib/rspec/mocks/error_generator.rb index 9bf0984f3..42ff35283 100644 --- a/lib/rspec/mocks/error_generator.rb +++ b/lib/rspec/mocks/error_generator.rb @@ -268,6 +268,17 @@ def unexpected_arguments_message(expected_args_string, actual_args_string) def error_message(expectation, args_for_multiple_calls) expected_args = format_args(expectation.expected_args) actual_args = format_received_args(args_for_multiple_calls) + + if RSpec::Support::RubyFeatures.distincts_kw_args_from_positional_hash? && expected_args == actual_args + expected_hash = expectation.expected_args.last + actual_hash = args_for_multiple_calls.last.last + if Hash === expected_hash && Hash === actual_hash && + (Hash.ruby2_keywords_hash?(expected_hash) != Hash.ruby2_keywords_hash?(actual_hash)) + actual_args += Hash.ruby2_keywords_hash?(actual_hash) ? " (keyword arguments)" : " (options hash)" + expected_args += Hash.ruby2_keywords_hash?(expected_hash) ? " (keyword arguments)" : " (options hash)" + end + end + message = default_error_message(expectation, expected_args, actual_args) if args_for_multiple_calls.one? diff --git a/spec/rspec/mocks/diffing_spec.rb b/spec/rspec/mocks/diffing_spec.rb index 3b1f91edf..e17aabcd3 100644 --- a/spec/rspec/mocks/diffing_spec.rb +++ b/spec/rspec/mocks/diffing_spec.rb @@ -83,6 +83,39 @@ end end + if RSpec::Support::RubyFeatures.distincts_kw_args_from_positional_hash? + eval <<-'RUBY', nil, __FILE__, __LINE__ + 1 + it "print a diff when keyword argument were expected but got an option hash (using splat)" do + with_unfulfilled_double do |d| + expect(d).to receive(:foo).with(**expected_hash) + expect { + d.foo(expected_hash) + }.to fail_with( + "# received :foo with unexpected arguments\n" \ + " expected: ({:baz=>:quz, :foo=>:bar}) (keyword arguments)\n" \ + " got: ({:baz=>:quz, :foo=>:bar}) (options hash)" + ) + end + end + RUBY + + eval <<-'RUBY', nil, __FILE__, __LINE__ + 1 + it "print a diff when keyword argument were expected but got an option hash (literal)" do + with_unfulfilled_double do |d| + expect(d).to receive(:foo).with(:positional, keyword: 1) + expect { + options = { keyword: 1 } + d.foo(:positional, options) + }.to fail_with( + "# received :foo with unexpected arguments\n" \ + " expected: (:positional, {:keyword=>1}) (keyword arguments)\n" \ + " got: (:positional, {:keyword=>1}) (options hash)" + ) + end + end + RUBY + end + if RUBY_VERSION.to_f < 1.9 # Ruby 1.8 hashes are not ordered, but `#inspect` on a particular unchanged # hash instance should return consistent output. However, on Travis that does