You're testing a service that depends on a collaborator with both void side-effect methods and value-returning methods. Walk through how you'd choose between strict mockk, relaxed = true, relaxUnitFun = true, and spyk, and the failure modes of each.
answer
- Axis = what unstubbed call does: throw/fake/real
- Default strict, add relaxUnitFun for void noise
- relaxed = silent false-green risk
- spyk = real + self-call gotcha + coupling
- Prefer loud failures over silent defaults
basics
~20 sDefault to strict mockk and stub only what you read. Use relaxUnitFun for noisy void calls. Use relaxed only for collaborators you never read returns from. Use spyk when you want mostly real behavior with one method replaced.
solid answer
~50 sStart strict (mockk<T>()) and stub each value-returning method the code-under-test reads with every { } returns; verify { } the side effects. If the collaborator has many fire-and-forget void methods (loggers, publishers), add relaxUnitFun = true to drop that boilerplate while keeping value methods strict — so a missing stub fails loudly. Reserve relaxed = true for collaborators you only verify and never consume a return from, because its failure mode is silent: an unexpected or mis-typed call returns 0/""/empty and the test passes for the wrong reason. Use spyk(real) only when you genuinely want the real implementation for most methods and must neutralize one nondeterministic/expensive call or assert an internal call — accepting the self-call dispatch gotcha and tighter coupling to implementation. The decision axis is default behavior (throw vs fake vs real) traded against the risk of masking bugs.
code
kotlin · 8 lines// relaxed's silent failure mode demonstrated
val repo = mockk<UserRepo>(relaxed = true)
// typo: real method is findById, we stub findByid -> never matches
every { repo.findByid(1) } returns User(1, "Ann")
val u = repo.findById(1) // unstubbed -> relaxed nested mock<User>, NOT Ann
// test may still pass on default fields -> false green
// strict would have THROWN, surfacing the typo immediatelygo deeper
Can name the four options but may default to relaxed for convenience without seeing the risk.
Chooses strict + stubs and uses relaxUnitFun for void noise; knows spy runs real code.
Articulates each option's failure mode, especially relaxed's silent false-green, and prefers loud failures; uses spyk sparingly.
Codifies a team policy (strict/relaxUnitFun default, relaxed banned-by-default, spyk reviewed) and ties it to test-suite reliability and regression detection.
## The decision axis Every option differs only in **what an unstubbed call does**: - `mockk<T>()` (strict) → **throws**. - `mockk<T>(relaxUnitFun = true)` → Unit calls no-op, value calls **throw**. - `mockk<T>(relaxed = true)` → all calls return **synthetic defaults**. - `spyk(real)` / `spyk<T>()` → all calls run the **real** method. Pick by trading boilerplate against the risk of a test passing for the wrong reason. ## Recommended decision flow 1. **Default: strict `mockk<T>()`.** Stub each value-returning method your SUT reads: ```kotlin val repo = mockk<UserRepo>() every { repo.findById(1) } returns User(1, "Ann") ``` If the SUT also calls a void `repo.touch(1)`, strict makes you stub or it throws. 2. **Many void calls? Add `relaxUnitFun = true`.** Now `repo.touch(1)` is a no-op, but `findById` still **throws** if you forget to stub it — value correctness stays protected. 3. **Only verifying, never reading? Consider `relaxed = true`** — but know the failure mode below. 4. **Want real behavior except one call? `spyk`.** ## Failure modes (the senior part) - **Strict**: failure mode is *over-specification* — brittle setup, noisy tests. Mitigate with relaxUnitFun. - **relaxed = true**: failure mode is **silent masking**. A typo (`every { repo.findByid(1) }` never matches) or an unexpected call returns `null`-free defaults; the SUT proceeds on `0`/empty and the assertion may still pass — a **false green**. This is the most dangerous because it hides regressions. - **relaxUnitFun = true**: minimal failure mode — only void calls are silently absorbed, which is usually intended; value methods still fail loudly. Best default-plus. - **spyk**: failure modes are (a) the **self-call gotcha** — internal `this.method()` calls bypass stubs; (b) **coupling to implementation** — you're now asserting on real internals; (c) running real side effects (network/IO) you forgot to stub; (d) shared mutable state when wrapping a live object. ## A concrete shape ```kotlin // Collaborator: fun save(u): Unit (void), fun nextId(): Long (value) val repo = mockk<UserRepo>(relaxUnitFun = true) // save() free, nextId() strict every { repo.nextId() } returns 7L val svc = UserService(repo) svc.create("Ann") verify { repo.save(match { it.id == 7L }) } ``` This keeps value correctness explicit, kills void boilerplate, and verifies the side effect. ## Heuristics to state aloud - **Read a return? Stub it explicitly** (don't let relaxed fake it). - **Side-effect only? verify it**; relaxUnitFun removes the stub noise. - **Need real logic? spyk**, but treat it as a smell signaling you might be testing internals. - Prefer the option whose failure mode is **loud** over one that's **silent**.
- Why is relaxUnitFun a safer default than relaxed for most service tests?It silences only void calls (intended) while value-returning methods still throw when unstubbed, so missing setup fails loudly instead of feeding wrong defaults into the SUT.
- Give a sign that reaching for spyk is a smell.If you must stub a private/internal method or assert on an internal self-call to make the test pass, you're coupling to implementation detail — refactor toward a real dependency or extract the collaborator.
It's a security-vs-convenience dial: strict locks every door (annoying but safe), relaxed leaves them all open (convenient but you won't notice an intruder), relaxUnitFun only props the harmless service door, and spyk staffs the building with real people you occasionally swap out.
saying these in an interview costs you the question
- Recommending relaxed = true as a blanket default to reduce boilerplate
- Not recognizing relaxed's silent false-green failure mode
- Treating spyk as interchangeable with a mock without mentioning real side effects/self-calls
- Stubbing void methods on a strict mock instead of using relaxUnitFun
- No mention of verify for side-effect-only collaborators