diff --git a/lib/minitest/mock.rb b/lib/minitest/mock.rb index ddfc788d..02411a5c 100644 --- a/lib/minitest/mock.rb +++ b/lib/minitest/mock.rb @@ -78,7 +78,7 @@ def initialize delegator = nil # :nodoc: # @mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment" # - def expect name, retval, args = [], &blk + def expect name, retval, args = [], **kwargs, &blk name = name.to_sym if block_given? @@ -86,7 +86,7 @@ def expect name, retval, args = [], &blk @expected_calls[name] << { :retval => retval, :block => blk } else raise ArgumentError, "args must be an array" unless Array === args - @expected_calls[name] << { :retval => retval, :args => args } + @expected_calls[name] << { :retval => retval, :args => args, :kwargs => kwargs } end self end @@ -94,7 +94,13 @@ def expect name, retval, args = [], &blk def __call name, data # :nodoc: case data when Hash then - "#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}" + args = data[:args].inspect[1..-2] + kwargs = data[:kwargs] + if kwargs && !kwargs.empty? then + args << ", " unless args.empty? + args << kwargs.inspect[1..-2] + end + "#{name}(#{args}) => #{data[:retval].inspect}" else data.map { |d| __call name, d }.join ", " end @@ -115,10 +121,10 @@ def verify true end - def method_missing sym, *args, &block # :nodoc: + def method_missing sym, *args, **kwargs, &block # :nodoc: unless @expected_calls.key?(sym) then if @delegator && @delegator.respond_to?(sym) - return @delegator.public_send(sym, *args, &block) + return @delegator.public_send(sym, *args, **kwargs, &block) else raise NoMethodError, "unmocked method %p, expected one of %p" % [sym, @expected_calls.keys.sort_by(&:to_s)] @@ -129,26 +135,31 @@ def method_missing sym, *args, &block # :nodoc: expected_call = @expected_calls[sym][index] unless expected_call then - raise MockExpectationError, "No more expects available for %p: %p" % - [sym, args] + raise MockExpectationError, "No more expects available for %p: %p %p" % + [sym, args, kwargs] end - expected_args, retval, val_block = - expected_call.values_at(:args, :retval, :block) + expected_args, expected_kwargs, retval, val_block = + expected_call.values_at(:args, :kwargs, :retval, :block) if val_block then # keep "verify" happy @actual_calls[sym] << expected_call - raise MockExpectationError, "mocked method %p failed block w/ %p" % - [sym, args] unless val_block.call(*args, &block) + raise MockExpectationError, "mocked method %p failed block w/ %p %p" % + [sym, args, kwargs] unless val_block.call(*args, **kwargs, &block) return retval end if expected_args.size != args.size then - raise ArgumentError, "mocked method %p expects %d arguments, got %d" % - [sym, expected_args.size, args.size] + raise ArgumentError, "mocked method %p expects %d arguments, got %p" % + [sym, expected_args.size, args] + end + + if expected_kwargs.size != kwargs.size then + raise ArgumentError, "mocked method %p expects %d keyword arguments, got %p" % + [sym, expected_kwargs.size, kwargs] end zipped_args = expected_args.zip(args) @@ -157,8 +168,23 @@ def method_missing sym, *args, &block # :nodoc: } unless fully_matched then - raise MockExpectationError, "mocked method %p called with unexpected arguments %p" % - [sym, args] + fmt = "mocked method %p called with unexpected arguments %p" + raise MockExpectationError, fmt % [sym, args] + end + + unless expected_kwargs.keys.sort == kwargs.keys.sort then + fmt = "mocked method %p called with unexpected keywords %p vs %p" + raise MockExpectationError, fmt % [sym, expected_kwargs.keys, kwargs.keys] + end + + fully_matched = expected_kwargs.all? { |ek, ev| + av = kwargs[ek] + ev === av or ev == av + } + + unless fully_matched then + fmt = "mocked method %p called with unexpected keyword arguments %p vs %p" + raise MockExpectationError, fmt % [sym, expected_kwargs, kwargs] end @actual_calls[sym] << { diff --git a/test/minitest/test_minitest_mock.rb b/test/minitest/test_minitest_mock.rb index 561b1a57..7404acd5 100644 --- a/test/minitest/test_minitest_mock.rb +++ b/test/minitest/test_minitest_mock.rb @@ -51,7 +51,7 @@ def test_blow_up_on_wrong_number_of_arguments @mock.sum end - assert_equal "mocked method :sum expects 2 arguments, got 0", e.message + assert_equal "mocked method :sum expects 2 arguments, got []", e.message end def test_return_mock_does_not_raise @@ -210,7 +210,7 @@ def test_method_missing_empty mock.a end - assert_equal "No more expects available for :a: []", e.message + assert_equal "No more expects available for :a: [] {}", e.message end def test_same_method_expects_are_verified_when_all_called @@ -252,6 +252,22 @@ def test_same_method_expects_with_same_args_blow_up_when_not_all_called assert_equal exp, e.message end + def test_handles_kwargs_in_error_message + mock = Minitest::Mock.new + + mock.expect :foo, nil, [:bar, 42], kw: true + + e = assert_raises ArgumentError do + mock.foo :bar + end + + # e = assert_raises(MockExpectationError) { mock.verify } + + exp = "mocked method :foo expects 2 arguments, got [:bar]" + + assert_equal exp, e.message + end + def test_verify_passes_when_mock_block_returns_true mock = Minitest::Mock.new mock.expect :foo, nil do @@ -270,11 +286,131 @@ def test_mock_block_is_passed_function_params a1 == arg1 && a2 == arg2 && a3 == arg3 end - mock.foo arg1, arg2, arg3 + assert_silent do + if RUBY_VERSION > "3" then + mock.foo arg1, arg2, arg3 + else + mock.foo arg1, arg2, **arg3 # oddity just for ruby 2.7 + end + end + + assert_mock mock + end + + def test_mock_block_is_passed_keyword_args__block + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil do |k1:, k2:, k3:| + k1 == arg1 && k2 == arg2 && k3 == arg3 + end + + mock.foo(k1: arg1, k2: arg2, k3: arg3) + + assert_mock mock + end + + def test_mock_block_is_passed_keyword_args__block_bad_missing + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil do |k1:, k2:, k3:| + k1 == arg1 && k2 == arg2 && k3 == arg3 + end + + e = assert_raises ArgumentError do + mock.foo(k1: arg1, k2: arg2) + end + + assert_equal "missing keyword: :k3", e.message # basically testing ruby + end + + def test_mock_block_is_passed_keyword_args__block_bad_extra + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil do |k1:, k2:| + k1 == arg1 && k2 == arg2 && k3 == arg3 + end + + e = assert_raises ArgumentError do + mock.foo(k1: arg1, k2: arg2, k3: arg3) + end + + assert_equal "unknown keyword: :k3", e.message # basically testing ruby + end + + def test_mock_block_is_passed_keyword_args__block_bad_value + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil do |k1:, k2:, k3:| + k1 == arg1 && k2 == arg2 && k3 == arg3 + end + + e = assert_raises MockExpectationError do + mock.foo(k1: arg1, k2: arg2, k3: :BAD!) + end + + exp = "mocked method :foo failed block w/ [] {:k1=>:bar, :k2=>[1, 2, 3], :k3=>:BAD!}" + assert_equal exp, e.message + end + + def test_mock_block_is_passed_keyword_args__args + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil, k1: arg1, k2: arg2, k3: arg3 + + mock.foo(k1: arg1, k2: arg2, k3: arg3) assert_mock mock end + def test_mock_block_is_passed_keyword_args__args_bad_missing + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil, k1: arg1, k2: arg2, k3: arg3 + + e = assert_raises ArgumentError do + mock.foo(k1: arg1, k2: arg2) + end + + assert_equal "mocked method :foo expects 3 keyword arguments, got %p" % {k1: arg1, k2: arg2}, e.message + end + + def test_mock_block_is_passed_keyword_args__args_bad_extra + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil, k1: arg1, k2: arg2 + + e = assert_raises ArgumentError do + mock.foo(k1: arg1, k2: arg2, k3: arg3) + end + + assert_equal "mocked method :foo expects 2 keyword arguments, got %p" % {k1: arg1, k2: arg2, k3: arg3}, e.message + end + + def test_mock_block_is_passed_keyword_args__args_bad_key + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil, k1: arg1, k2: arg2, k3: arg3 + + e = assert_raises MockExpectationError do + mock.foo(k1: arg1, k2: arg2, BAD: arg3) + end + + assert_includes e.message, "unexpected keywords [:k1, :k2, :k3]" + assert_includes e.message, "vs [:k1, :k2, :BAD]" + end + + def test_mock_block_is_passed_keyword_args__args_bad_val + arg1, arg2, arg3 = :bar, [1, 2, 3], { :a => "a" } + mock = Minitest::Mock.new + mock.expect :foo, nil, k1: arg1, k2: arg2, k3: arg3 + + e = assert_raises MockExpectationError do + mock.foo(k1: arg1, k2: :BAD!, k3: arg3) + end + + assert_match(/unexpected keyword arguments.* vs .*:k2=>:BAD!/, e.message) + end + def test_mock_block_is_passed_function_block mock = Minitest::Mock.new block = proc { "bar" } @@ -286,6 +422,13 @@ def test_mock_block_is_passed_function_block assert_mock mock end + def test_mock_forward_keyword_arguments + mock = Minitest::Mock.new + mock.expect(:foo, nil) { |bar:| bar == 'bar' } + mock.foo(bar: 'bar') + assert_mock mock + end + def test_verify_fails_when_mock_block_returns_false mock = Minitest::Mock.new mock.expect :foo, nil do @@ -293,7 +436,7 @@ def test_verify_fails_when_mock_block_returns_false end e = assert_raises(MockExpectationError) { mock.foo } - exp = "mocked method :foo failed block w/ []" + exp = "mocked method :foo failed block w/ [] {}" assert_equal exp, e.message end