skip to content

Beyond the arguments, what invocation metadata does MockK expose inside an `answers { }` block, and when is reaching for `call`, `self` or `nArgs` actually justified?

level: seniorimportance: nice to knowfreq 25%

answer

  1. scope = call, invocation, self, method, args, nArgs, matcher
  2. typed accessors first; metadata only when the signature varies
  3. self ⇒ fluent builders returning this
  4. method.name + args = fast stub debugging
  5. branching on self/method.name ⇒ write a fake instead

basics

~20 s

The answer scope exposes the whole invocation: call, call.invocation (with self, method and args), plus shortcuts args and nArgs. Justified uses are answers shared across overloads or several mocks, and diagnostics. If a stub needs the invoking object's identity to decide, that is usually a smell.

solid answer

~60 s

The receiver of an `answers { }` block is not just an argument bag. It carries: - **`call`** — the whole call object, including the matcher that selected this stub; - **`call.invocation`** (also reachable as `invocation`) — the concrete invocation: `self` (the mock the call landed on), `method` (a method description with its name), and `args`; - **`args`** and **`nArgs`** — shortcuts for the raw argument list and its size. The honest answer about when to use them: rarely. Typed accessors cover almost everything. The real cases are (1) an answer lambda defined once and reused across several stubs or overloads, where arity and argument types are not fixed, so you branch on `nArgs` or read `args` generically; (2) diagnostics — printing `method.name` and `args` while debugging why a stub matched; (3) very occasionally, returning the receiver itself for a fluent builder API, via `self`. Anything more elaborate — branching on which mock was called to decide business behavior — means the stub has become a hidden implementation and should be a real fake instead.

code

kotlin · 6 lines
kotlin
val renderer = mockk<Renderer>()
every { renderer.render(any()) } answers { args.joinToString("|") }
every { renderer.render(any(), any()) } answers { args.joinToString("|") }

val builder = mockk<QueryBuilder>()
every { builder.where(any()) } answers { self as QueryBuilder }  // keeps chaining alive

go deeper

for a junior

It is enough to know the block can see more than the arguments — the invocation, including which mock was called and which method.

for a middle

Name the members — call, invocation, self, method, args, nArgs — and give the ordinary uses: generic answers over overloads and quick debugging.

for a senior

Discuss when the metadata is justified versus when it signals the stub has become an implementation, and note that exceptions and assertions inside the block propagate into the code under test.

for a principal

Draw the line for the team: stubs stay declarative, dispatch logic belongs in a fake, and tests should not couple to library internals without a reason.

## The scope is a view on the invocation MockK's answer block executes with a receiver that wraps the intercepted call. Alongside the typed argument accessors, the scope exposes the invocation itself: - **`call`** — the call object MockK built for this invocation. It carries both the invocation and the `matcher` (`InvocationMatcher`) that caused this stub to be chosen. The lambda form of `answers` is also passed the call as its parameter, so `answers { call -> … }` and using the receiver's `call` are the same object. - **`invocation`** — shorthand for `call.invocation`. Its notable members are `self` (the mock instance the call was made on), `method` (a `MethodDescription`, whose `name` is the useful part), and `args` (the `List<Any?>` of arguments). - **`args`** / **`nArgs`** — direct shortcuts to the argument list and its size. There is also `matcher` for the matcher that selected the stub. None of this is exotic machinery: it is the same data MockK prints in its failure messages, exposed to you. ## Where it earns its place ### A shared answer across overloads or mocks When several stubs want the same behavior and their signatures differ, a typed accessor cannot be written once. A generic block can: ```kotlin val echoLast: MockKAnswerScope<Any, Any>.(Call) -> Any = { args.last()!! } ``` More commonly, in-line branching on arity: ```kotlin every { renderer.render(any()) } answers { render(args) } every { renderer.render(any(), any()) } answers { render(args) } ``` where `render` is a helper in the test that handles a variable-length list. This keeps a fixture small when a legacy interface has five overloads of the same operation. ### Diagnostics while a stub misbehaves When a stub matches calls you did not expect (or does not match ones you did), a temporary `println("${invocation.method.name}(${args})")` inside the answer block tells you exactly what arrived. It is a debugging aid, removed before commit, and it is often faster than reasoning about matcher precedence from the source. ### Fluent/builder collaborators A builder whose methods return `this` can be stubbed with `answers { self }` so chaining keeps working without stubbing each step to return the mock by name. This is a genuine, if niche, use of `self`. ## Where it is a smell If a block inspects `self` to decide *what business value to return* — "if this is the primary repo return X, if the replica return Y" — the test has grown a miniature implementation inside a stub. Two better options: register two separate stubs, one per mock, each with a plain `returns`; or write a small fake class implementing the interface. The same applies to branching on `method.name`: at that point you are writing a dispatcher, and a hand-written double is more readable, debuggable and reusable. A second caution is coupling to MockK's internals. `Call`, `Invocation` and `MethodDescription` are library types; tests that reach deep into them are more exposed to library changes than tests that use the documented argument accessors. Prefer `firstArg`/`arg(n)` whenever the signature is known. ## Interactions worth knowing - The block runs **per call**, so anything you compute from the invocation is per-invocation — you can accumulate into a list the test owns, giving a poor man's capture. - Exceptions thrown inside the block propagate to the caller as if the mocked function threw. That is what makes `answers { throw … }` a legitimate failure injection, and also why assertions inside the block are risky: production code with a broad `catch` can swallow the assertion error and leave the test green. - The same scope and metadata are available in `andThenAnswer { }` for later entries of a sequence, and in the coroutine-aware block form for suspending functions. ## Summary The metadata surface exists, it is small — `call`, `invocation`, `self`, `method`, `args`, `nArgs`, `matcher` — and its legitimate uses are generic/shared answers, debugging, and fluent receivers. Reaching for it to encode business branching means the double has outgrown a stub.

  • You see a stub whose answers block branches on invocation.method.name to return different values. What do you suggest?
    Split it into separate stubs, one per method, each with a plain returns — MockK already dispatches by method, so the branching duplicates the library's job. If the behavior is genuinely stateful across methods, replace the mock with a small hand-written implementation of the interface, which is easier to read and debug than a dispatcher hidden in a lambda.
  • Is putting an assertion inside an answers block a good way to check arguments?
    Usually not. The assertion error is thrown from inside the mocked call, so any broad catch in the code under test can swallow it and the test stays green while the check silently failed. Capture the argument and assert after the exercise phase, where the failure is unambiguous and the message shows the expected and actual values.

saying these in an interview costs you the question

  • Thinking the answers block only has access to arguments, not the invocation.
  • Using self or method.name to encode business logic instead of registering separate stubs.
  • Assuming an assertion failure inside an answer block always fails the test.
  • Believing args is one-based or that nArgs excludes the receiver's own arguments.
  • Reaching into MockK's Call/Invocation types when a typed accessor would do.

context