Ran into an interesting problem today in which a method in one of my classes happened to have the same name as a method on my testing framework.
I'm pasting a lot of code but highlighting the important details with comments.
Contract
require_relative './exceptions'
require_relative './fulfillment_agent'
module IhliTL
class Contract
attr_reader :parent
def initialize(contract_definition, parent = nil)
@name = contract_definition[:name]
@clauses = init_clauses(contract_definition[:clauses])
@fulfillment_agents = contract_definition[:fulfillment_agents]
@contracts = contract_definition[:contracts]
end
def verify(payload)
@clauses.map do |clause|
clause[:assertions].map do |assertion|
#####
# Our 'Verifier' class here has a method
# named 'verify' which is also a method
# used by MiniTest::Mock.
# A problem when we try to mock this dependency...
#####
clause[:verifier].verify(assertion)
end
end
end
def init_clauses(clause_definitions)
clause_definitions.map do |clause|
clause[:verifier].verify(clause[:assertions])
end
end
end
end
Test Contract
class TestContract < MiniTest::Test
def setup
@mock_verifier = MiniTest::Mock.new
@contract_definition = {
name: 'Test Contract',
clauses: [
name: 'Test Clause',
verifier: @mock_verifier,
assertions: [],
],
fulfillment_agents: [],
contracts: []
}
end
def test_verifier
#####
# Here we are setting up an expectation
# on our mocked 'Verify' class,
# but the method is already defined
# on the 'Mock' class itself...
# Hmm... What will happen?
#####
@mock_verifier.expect :verify, nil, [@assertion]
@contract = IhliTL::Contract.new @contract_definition
@contract.verify({})
@mock_verifier.verify
end
end
Error
1) Error:
TestContract#test_resolve_verifies_each_clause_with_payload:
ArgumentError: wrong number of arguments (1 for 0)
The verify
method on our class is being overridden by the verify
method on MiniTest::Mock. The one on MiniTest::Mock expects 0 arguments and we are passing in 1 to the method in our class.
Solution
In out setup, we need to open up our instance of MiniTest::Mock and define a new method on it which isn't used in our code. I chose assert
Update
The original code (included at the bottom of this post) is wrong. The tests pass incorrectly because it doesn't actually assert anything.
A working solution is as follows:
@mock_verifier.instance_eval {
def assert
@expected_calls.each do |name, expected|
actual = @actual_calls.fetch(name, nil)
raise MockExpectationError, "expected #{__call name, expected[0]}" unless actual
raise MockExpectationError, "expected #{__call name, expected[actual.size]}, got [#{__call name, actual}]" if
actual.size < expected.size
end
true
end
}
*** End Update ***
Leaving the rest here for reference
We use Ruby's Object#method method to store off the value of verify
and it's context. We return this from our assert
method.
We then have to remove the MiniTest::Mock version of the verify
method using instance_eval 'undef :verify'
to free up the method for our class.
def @mock_verifier.assert
@mock_verifier.method(:verify)
end
@mock_verifier.instance_eval 'undef :verify'
Whoops. Now we get this error.
2) Error:
TestContract#test_resolve_verifies_each_clause_with_payload:
NameError: undefined method `verify' for class `NilClass'
/Users/eihli/Projects/ihlitl/test/test_contract.rb:12:in `method'
/Users/eihli/Projects/ihlitl/test/test_contract.rb:12:in `assert'
It looks like @mock_verifier
inside the def mock_verifier.assert
block is referring to an instance variable @mock_verifier
on the TestContract < MiniTest::Test
class, not on the instance of MiniTest::Mock
which we are trying to redefine.
I think we need to wrap that method in a Proc so that it gets evaluated in the context of the instance that's calling it.
def @mock_verifier.assert
-> { @mock_verifier.method(:verify) }
end
@mock_verifier.instance_eval 'undef :verify'
And ta-da, we have a successfully failing spec :)
2) Error:
TestContract#test_resolve_verifies_each_clause_with_payload:
MockExpectationError: mocked method :verify called with unexpected arguments [[{:msg_chain=>["[]"], :args=>["test_arg"], :comparator=>"==", :value=>"test_value"}]]