With a relaxed MockK mock, a chain like `service.repo().findAll()` runs without any stubbing at all. What object does `repo()` hand back, is it the same object on every call, and how do you verify calls made on it?
answer
- relaxed → child mock → itself relaxed → chains just work
- same call + same args → same child instance
- different args → different children
- verify by capturing: val repo = service.repo()
- implicit deep stubs hide the collaborator graph
basics
~20 sIt hands back a child mock — a new mock of the return type, itself relaxed. MockK remembers child mocks, so the same call with the same arguments returns the same instance. To verify, capture that instance (val repo = service.repo()) and verify through the reference.
solid answer
~50 sBecause a relaxed mock answers non-trivial return types with a fresh relaxed mock, chains become **implicit deep stubs**: `repo()` returns a relaxed `Repository`, whose `findAll()` in turn returns a relaxed default. MockK keeps these child mocks rather than fabricating a new one each time, so `service.repo() === service.repo()` — the same call signature yields the same child. (`clearMocks` exposes this with its `childMocks` flag.) Different arguments produce different children, which is easy to trip over: `service.repoFor("a")` and `service.repoFor("b")` are two distinct mocks. To verify, hold the child: `val repo = service.repo(); verify { repo.findAll() }`. Anonymous mid-chain mocks are otherwise unreachable. Judgement: implicit deep stubs are cheap but they hide the collaborator graph — the test asserts nothing about the shape of what it traverses, and a refactor that changes the chain silently keeps passing. Stub the chain explicitly, or hand the class the collaborator directly.
code
kotlin · 8 linesval service = mockk<Service>(relaxed = true)
val repo = service.repo()
check(repo === service.repo()) // same call signature -> same child mock
systemUnderTest(service).run()
verify { repo.findAll() } // reachable only because we captured itgo deeper
Know that a relaxed mock returns another relaxed mock for object return types, which is why an unstubbed chain runs at all.
Explain child-mock identity — stable per call signature, argument-sensitive — and demonstrate verifying by capturing the child or stubbing the chain explicitly.
Argue the trade: implicit deep stubs hide the collaborator graph and let chain refactors pass silently, and retained children are state that must be cleared between tests.
Set the norm — depend on the collaborator you actually need rather than excavating it through a chain — and treat frequent deep chains in tests as feedback about coupling in production code.
## Where child mocks come from Relaxed mode must produce a value for every unstubbed call. For a return type it cannot zero out — a domain class, an interface, another service — it produces **a new mock of that type, created relaxed as well**. MockK calls these *child mocks*. The recursion is what makes chains work with zero setup: ```kotlin val service = mockk<Service>(relaxed = true) service.repo().findAll() // no every {} anywhere ``` `repo()` is unstubbed, so relaxed mode manufactures a relaxed `Repository`; `findAll()` on that child is unstubbed too, so it produces its own relaxed default. This is effectively **deep stubbing**, arrived at implicitly rather than requested. ## Identity: the same call gives the same child MockK does not mint a new object on every invocation. Child mocks are remembered per call signature, so: ```kotlin check(service.repo() === service.repo()) // true ``` That stability is essential: without it, a child mock the production code obtained would be a different object from the one your test obtained, and verification could never work. Direct evidence that MockK holds this state is the `childMocks` flag on `clearMocks`, which exists precisely to drop these retained children. The corollary is that **arguments are part of the identity**. `service.repoFor("orders")` and `service.repoFor("users")` produce two different child mocks; calls recorded on one are invisible to the other. Tests that assume "the child mock" is a single object get confusing verification failures when the production code passes a different argument than the test did. ## Verifying calls on a child mock A mid-chain mock is anonymous — you never named it, so you cannot mention it in a `verify` block. Two workable approaches: 1. **Capture it by making the same call in the test.** Because the child is stable, calling `service.repo()` in the test returns the very object production code used: ```kotlin val repo = service.repo() systemUnderTest.run() verify { repo.findAll() } ``` Careful: your own `service.repo()` call is itself recorded on `service`, so exhaustive verification of `service` must account for it. 2. **Stub the chain explicitly**, which is usually clearer anyway: ```kotlin val repo = mockk<Repository>() every { service.repo() } returns repo every { repo.findAll() } returns listOf(account) ``` Now the intermediate object is a named participant in the test, its behaviour is stated, and verification reads naturally. ## Why implicit deep stubs deserve suspicion They are convenient and they hide information: - **The collaborator graph disappears.** A reader cannot tell from the test what `service` is expected to expose, or that the code under test walks two hops to get its data. - **Failures move away from the cause.** A relaxed child returns a relaxed value, which returns another relaxed value; the eventual assertion fails on something several steps removed from the unstubbed call that caused it. - **Refactors pass silently.** Change the chain — insert a hop, rename the accessor, return a different type — and the test still passes, because every shape of chain is auto-satisfied. The test has stopped constraining the design. - **Train-wreck code gets easier to keep.** A chain like `a().b().c()` is usually a Law-of-Demeter problem; making it painless to test removes the pressure to fix it. The healthier default is: mock the thing the class actually depends on, and let the test show that dependency. If the class needs a `Repository`, hand it a `Repository`, not a `Service` from which one can be excavated. ## Lifecycle note Because children are retained, they are part of a mock's state between tests. `clearMocks(mock, childMocks = true)` is the lever for dropping them; leaving stale children around is one of the quieter forms of test-to-test leakage. (Lifecycle mechanics as a whole are a topic of their own; the point here is simply that child mocks *are* retained state.) ## Interview framing Say: *a child mock, relaxed, remembered per call signature so the same call returns the same instance — arguments included in the identity. Verify by capturing it or by stubbing the chain explicitly. And treat implicit deep stubs as a signal: they hide the collaborator graph and let chain refactors pass unnoticed.*
- Are two child mocks obtained from the same method but different arguments the same object?No. The child is keyed by the call, arguments included, so `repoFor("orders")` and `repoFor("users")` yield two distinct relaxed mocks with independent recorded calls. A test that captures one and verifies against calls production code made on the other fails with a confusing "no calls recorded" message, and the fix is to capture with the same arguments the production code uses — or to stub the call explicitly.
- What is the argument against letting relaxed chains stand in for explicit stubbing?The test stops describing the dependency. Nothing in it states what `service` must expose or how many hops the code traverses, so a refactor that changes the chain still passes, and a failure surfaces several steps away from the unstubbed call that caused it. Explicit stubs cost two lines and make the collaborator graph — and any Law-of-Demeter problem in it — visible in the test.
saying these in an interview costs you the question
- "Every call to repo() creates a brand-new mock, so verification is impossible" — children are remembered per call signature.
- "The child mock is strict, so the next call in the chain will fail" — children inherit relaxation.
- "You can verify a mid-chain call without holding a reference to the child" — an anonymous mock cannot be named in a verify block.
- "Child mocks are per method, regardless of arguments" — arguments are part of the child's identity.
- Treating implicit deep stubs as good practice rather than as a convenience that hides the collaborator graph.