Overriding the 'verify' method on MiniTest::Mock

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"}]]
Show Comments