Skip to content

Commit

Permalink
- Extended Mock#expect to record kwargs.
Browse files Browse the repository at this point in the history
- Extended Mock#__call to display kwargs.
- Extended Mock#method_missing to take kwargs & compare them against expected.
+ Mock#method_missing displays better errors on arity mismatch.
Added a buuunch of tests for mocking

[git-p4: depot-paths = "//src/minitest/dev/": change = 13434]
  • Loading branch information
zenspider committed Jun 13, 2022
1 parent 52b9557 commit 6e06ac9
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 19 deletions.
56 changes: 41 additions & 15 deletions lib/minitest/mock.rb
Expand Up @@ -78,23 +78,29 @@ 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?
raise ArgumentError, "args ignored when block given" unless args.empty?
@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

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
Expand All @@ -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)]
Expand All @@ -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)
Expand All @@ -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] << {
Expand Down
151 changes: 147 additions & 4 deletions test/minitest/test_minitest_mock.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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" }
Expand All @@ -286,14 +422,21 @@ 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
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
Expand Down

0 comments on commit 6e06ac9

Please sign in to comment.