With minitest-mock, how do Minitest::Mock#expect and #verify work, and what happens when code calls the mock differently than expected?
answer
- expect(name, retval, args = [])
- args matched with === or ==
- one expect per call, in order
- verify raises MockExpectationError
- assert_mock turns it into a Failure
basics
~20 sexpect(name, retval, args) records one expected call and its return value; each actual call consumes one expectation. verify raises MockExpectationError for any expected call that never happened; unexpected arguments raise it at call time, unknown methods raise NoMethodError.
solid answer
~40 s`Minitest::Mock.new` builds an object with almost every method undefined. `mock.expect(:charge, :ok, [2_500], customer_id: "c-7")` records one expected call: name, return value, then an **array** of positional arguments and any keywords. Arguments are compared with `===` then `==`, so `[Integer]` or `[String]` match any value of that class. Each expect covers **one** call, used in order; a second call needs a second `expect`. At call time, wrong argument values raise `MockExpectationError`, a wrong count raises `ArgumentError`, an extra call raises `MockExpectationError` (`No more expects available`), and an unknown method raises `NoMethodError`. At the end, `mock.verify` raises `MockExpectationError` if an expected call never happened, or `assert_mock mock` reports the same thing as a Failure.
code
ruby · 15 linesrequire "minitest/autorun"
require "minitest/mock"
class RenewalServiceTest < Minitest::Test
def test_retries_once_after_a_decline
gateway = Minitest::Mock.new
gateway.expect(:charge, :declined, [2_500], customer_id: "c-7")
gateway.expect(:charge, :ok, [2_500], customer_id: "c-7")
result = RenewalService.new(gateway: gateway).renew(subscription)
assert_equal :renewed, result
assert_mock gateway
end
endgo deeper
Recall the order expect(name, return_value, [args]) and that verify or assert_mock must be called at the end of the test.
Explain === argument matching, one expectation per call, the block form, and which mismatch raises MockExpectationError, ArgumentError or NoMethodError.
Spot mock-based tests that stay green after the real collaborator's signature changes, and swallowed MockExpectationErrors hidden by broad rescues.
Decide where interaction checks with Minitest::Mock earn their brittleness, and where state-based tests or fakes give a team cheaper confidence.
## The object `Minitest::Mock` comes from the minitest-mock gem (`require "minitest/mock"`). A new mock undefines nearly every inherited instance method, so any call it has not been told about falls through to its `method_missing`. There it is checked against the list of expectations. A handful of methods (`class`, `inspect`, `send`, `===` and a few more) are kept but can still be mocked by name. ## Recording expectations `expect name, retval, args = [], **kwargs, &blk` adds one expected call and returns the mock, so calls can be chained: ```ruby gateway = Minitest::Mock.new gateway.expect(:charge, :ok, [2_500], customer_id: "c-7") ``` The rules: - **`args` must be an Array**, even for one argument; otherwise `expect` raises `ArgumentError, "args must be an array"`. - **Matching uses `===`, then `==`.** `[Integer]` accepts any integer, `[/^c-/]` any string the regexp matches, `[2_500]` only that value. Keywords are matched the same way. - **One expect equals one call.** Expectations for the same name are consumed in the order they were defined; call it twice, expect it twice. - **Block form** for anything `===` cannot express: `gateway.expect(:charge, :ok) { |amount, customer_id:| amount.positive? && customer_id.start_with?("c-") }`. The block receives the actual arguments and must return truthy. With a block, `args` and `kwargs` must be empty. ## What happens at call time | call | result | |---|---| | matches the next expectation | returns `retval` and records the call | | right name, wrong argument values | `MockExpectationError`: called with unexpected arguments | | right name, wrong number of arguments | `ArgumentError`: expects N arguments | | more calls than expectations | `MockExpectationError`: No more expects available | | a name never expected | `NoMethodError`: unmocked method, expected one of [...] | `MockExpectationError` is a top-level class that inherits from `StandardError`. Two consequences follow. A test that hits one of these errors is reported as an **Error**, not a Failure. And code under test with a broad `rescue => e` can **swallow** it, so the test never sees the mismatch at call time; `verify` at the end is what still catches the missing expected call. ## Verifying `verify` walks every expected name and raises `MockExpectationError` if any expected call is missing, for example `Expected charge(2500, customer_id: "c-7") => :ok`. It returns `true` when all expectations were met. Forgetting `verify` is the classic bug: the test passes even though the collaborator was never called. `assert_mock mock` wraps `verify`: it counts as an assertion and turns a `MockExpectationError` into a Failure with the same message. The spec form is `_(gateway).must_verify`. ## A worked renewal test ```ruby def test_renewal_charges_the_card gateway = Minitest::Mock.new gateway.expect(:charge, :ok, [Integer], customer_id: "c-7") RenewalService.new(gateway: gateway).renew(@subscription) assert_mock gateway end ``` ## Common mistakes - **Swapping the arguments**: `expect(:charge, [2_500], :ok)` passes a Symbol where the args Array belongs and raises `ArgumentError` at once; the order is name, return value, arguments. - **Keywords written as a trailing hash**: since Ruby 3.0 separated keyword arguments, `expect(:charge, :ok, [2_500, {customer_id: "c-7"}])` expects a positional Hash. Code that calls `charge(2_500, customer_id: "c-7")` passes a keyword instead, and the mock raises `ArgumentError` for the argument-count mismatch. Put keywords after the array: `expect(:charge, :ok, [2_500], customer_id: "c-7")`. - **Reusing one mock across tests**: expectations are consumed; build a new mock in each test or in `setup`. - **Skipping verification** because the test already asserts on a return value: the return value may come from the mock without the call ever happening in the right place. ## Delegation and limits - `Minitest::Mock.new(real_gateway)` makes a partial mock: unexpected methods the delegator responds to are forwarded to it instead of raising `NoMethodError`. - The minitest-mock README states that mocks do **not** support multi-threading; code that calls a mock from another thread should be tested with plain Ruby (a local flag, a `Queue`). - A mock checks names and arguments only. It does not know the real class's method signatures, so a renamed method on the real gateway leaves the mock-based test green.
- With Minitest::Mock, why might a mismatched call not fail the test at call time?The mismatch raises `MockExpectationError`, a `StandardError`. If the code under test wraps the gateway call in `rescue => e`, it swallows that error and carries on. The mismatched call is never recorded as satisfying the expectation, so `verify` or `assert_mock` at the end still fails, which is one more reason never to skip them.
- With Minitest::Mock, how do you accept any customer id but still check the amount?Use a class or pattern in the expectation, because arguments are compared with `===`: `gateway.expect(:charge, :ok, [2_500], customer_id: String)`. For rules `===` cannot express, pass a block instead of args; it receives the actual arguments and must return truthy.
- With Minitest::Mock, what does Minitest::Mock.new(real_object) change?It makes the mock delegate: a call to a method with no expectation is forwarded to `real_object` if it responds to it, instead of raising `NoMethodError`. Expected methods are still intercepted and verified. It is useful when only one call on a collaborator matters.
saying these in an interview costs you the question
- One expect call covers every call to that method name.
- Mocks verify themselves automatically when the test ends.
- The third argument to expect can be a bare value instead of an array.
- An unexpected method call on a mock returns nil.
- MockExpectationError cannot be rescued by rescue => e.