skip to content

What does MockK do internally when a test writes a stub over a chain of calls, such as `every { repo.session(any()).user.name } returns "ann"`, and what constraints does that place on the intermediate types?

level: seniorimportance: should knowfreq 40%

answer

  1. recording mode needs a receiver → child mock invented
  2. each hop registered as an answer, and recorded
  3. different matchers → different children
  4. children are strict unless root is relaxed
  5. primitive/String link → cannot deep-stub

basics

~20 s

MockK walks the chain during recording and auto-creates a child mock for each intermediate return, registering it as that call's answer. So every level becomes stubbed and verifiable, and each intermediate type must itself be mockable.

solid answer

~50 s

Inside `every { }` the mock is in recording mode. MockK evaluates the chain left to right; to continue past `repo.session(any())` it needs a non-null object, so it **creates a child mock** of the return type and registers it as the answer for that call with those matchers. It then records `user` on that child, creating another child, and finally attaches your answer to `name`. Consequences worth stating: - Every intermediate level is now genuinely stubbed — production code calling `repo.session(id)` gets the child mock even if it never touches `name` — and every intermediate call is recorded, so `verify { repo.session(id) }` passes. - Different argument matchers make different children: `session(1)` and `session(2)` are separate child mocks. - Child mocks are strict, so any other call on them fails with *no answer found* unless the root mock was relaxed. - Intermediate types must be mockable; a chain passing through a primitive or `String` cannot be deep-stubbed and must be built level by level.

code

kotlin · 7 lines
kotlin
every { repo.session(any()).user.name } returns "ann"

// all of these now hold:
val s = repo.session("id-1")   // a child mock
val u = s.user                  // another child mock
assertEquals("ann", u.name)
verify { repo.session("id-1") }

go deeper

for a junior

Know that MockK lets you stub a chain in one every and that it invents the in-between objects for you.

for a middle

Explain that each hop gets an auto-created child mock registered as its answer, so intermediates are both stubbed and verifiable.

for a senior

Add the sharp edges: matcher-partitioned children, strict child behaviour, non-mockable links forcing level-by-level stubbing, and how to read a chain failure.

for a principal

Weigh it as a coupling decision — a chained stub encodes a navigation path into the test, and you decide when that is acceptable versus restructuring the collaborator.

## Recording mode is the whole story `every { ... }` does not evaluate against a real object. The lambda runs with the mock in *recording* mode: each call it makes is captured — receiver, method, argument matchers — instead of being executed. For a single call that is straightforward. For a chain, MockK has a problem: to record `.user` it must first have *something* to call `user` on, because the Kotlin expression really does have to produce a receiver. MockK solves it by fabricating one. When the recorded call `repo.session(any())` returns a mockable type, MockK creates a **child mock** of that type, registers it as the answer for `session(any())` on `repo`, and continues recording against it. The same happens for `.user`. Only the final call in the chain gets *your* answer. ## What that means in practice **Every level really is stubbed.** After the single `every` above, calling `repo.session(id)` in production code returns a live child mock, whether or not the code then reads `user`. There is no lazy "only if the whole chain is walked" behaviour. **Every level is recorded, so it is verifiable.** `verify { repo.session(id) }` passes, and so does `verify { repo.session(id).user }` — which is worth knowing, because it means chained stubbing quietly widens what your verifications can assert on. **Argument matchers partition the children.** `every { repo.session(1).user.name } returns "a"` and `every { repo.session(2).user.name } returns "b"` create two independent child chains. A call with an argument matching neither hits the strict mock's *no answer found*. This is the most common confusion in practice: the code navigated with a different argument than the chain you stubbed, and the error names the intermediate call, not the leaf you were thinking about. **Child mocks are strict.** They answer exactly the calls the chain recorded. Call anything else on the returned intermediate and you get a *no answer found* naming a mock you never wrote down. If the root mock was created relaxed, that relaxation carries to the children, which makes the failure disappear — and also makes it much easier to write a test that silently exercises nothing. **Identity is stable.** A repeated call matching the same recorded matchers returns the same child instance, so code that caches an intermediate and calls it twice behaves consistently. ## The type constraint MockK can create a child mock only for a type it can instrument. Interfaces, open classes and final Kotlin classes are all fine — inline instrumentation is exactly what MockK buys you over subclass-proxy mockers. What does not work is a link in the chain whose type has no meaningful mock: primitives and their boxed forms, and core JDK value types the JVM will not let you retransform. If `repo.session(id)` returned a `String`, there is no child mock to build and the chain cannot be expressed in one `every`. The fix is mundane: stop deep-stubbing at that point. Create the intermediate value yourself — a real object, a data class instance, a plain string — stub the first hop to return it, and stub the next hop separately. Two explicit stubs are also usually easier to read than one long chain. Suspend links follow the same model but must be recorded with `coEvery`, since a `suspend` call cannot be recorded in a non-suspending block. ## Nullability If an intermediate is declared nullable, the chain still yields a non-null child mock — MockK is producing the answer, not the real code path. A test that wanted to exercise the null branch must stub that level explicitly to return `null` rather than relying on the chain. ## Diagnosing chain failures When a deep-stubbed test fails, read the error's *receiver*, not just the method. If it names an intermediate mock (`Session(child of Repo#1#2).user`), the chain broke before your leaf: either the argument did not match, the code took a different navigation path, or the intermediate was re-stubbed elsewhere. Printing the failing call and comparing it to the recorded chain hop by hop resolves nearly all of these. Finally, child mocks are part of the root mock's state; MockK's clearing API has a dedicated switch for them, so resetting a mock between tests can either keep or discard the auto-created children.

  • After `every { a.b(1).c() } returns x`, what happens when production code calls `a.b(2).c()`?
    The call to `b(2)` does not match the recorded matcher, so there is no registered answer for it and a strict mock fails with *no answer found* on `b(2)` — before `c()` is ever reached. The error names the intermediate call, which is why chain failures often look like they are about the wrong method. Stubbing with `any()` at that level, or adding a second chain, fixes it.
  • Does chained stubbing work if one hop is a `suspend` function?
    Yes, but the whole chain has to be recorded in a suspending recording block — `coEvery` — because a `suspend` call cannot be invoked inside the ordinary `every` lambda. The child-mock mechanics are identical; only the recording block changes.

saying these in an interview costs you the question

  • Thinking the intermediate calls are "virtual" and not really stubbed or recorded.
  • Expecting one child mock per method regardless of the arguments used.
  • Assuming an unstubbed method on an auto-created child returns null or a default on a strict mock.
  • Claiming MockK can deep-stub through any type, including String or primitives.
  • Confusing the child mock with the real object the production code would have returned.

context