MockK evaluates the lambda passed to every or verify under a call recorder, and may execute it more than once while it resolves the call signature. Given that, how would you factor shared stubbing and verification helpers across a large Kotlin test suite?
answer
- recording block = description, never work
- MockKMatcherScope receiver = sanctioned matcher reuse
- whole-statement helpers are the default unit
- slots outside, capture() inside
- one mock call per block; all-matchers in shared helpers
basics
~20 sTreat recording blocks as pure descriptions: one mock call, no side effects, nothing built inside them. Share matchers as MockKMatcherScope extension functions called inside the block, and share whole stubs as ordinary functions that wrap the entire every/returns statement.
solid answer
~60 sTwo rules drive the design. Recording lambdas may run more than once, so they must be **side-effect free**; and matchers are only meaningful inside the recorder's scope, so they cannot be passed in from outside. That leaves exactly two reuse units: - **Matcher-level:** `fun MockKMatcherScope.anyActiveUser() = match<User> { it.active }`, called as an argument inside the block. The receiver type is what makes it legal, and it keeps domain-specific predicates named and testable. - **Statement-level:** a plain function that owns the whole statement — `fun Repo.stubFind(user: User) { every { findById(any()) } returns user }` — or a `verifySaved(user)` helper containing the whole `verify(exactly = 1) { ... }`. This is the unit most suites should standardise on, because the caller never touches the recording window. What you must not build: helpers that construct matchers before the block, blocks that create slots, mutate counters, log, or call the system under test, and loops that generate matchers. Slots are created outside and captured inside. Keep one mock call per block so failures name one thing.
code
kotlin · 10 lines// matcher-level reuse: legal only inside a recording block
fun MockKMatcherScope.anyActiveUser() = match<User> { it.active }
// statement-level reuse: caller never opens a recording window
fun Repo.stubSaveSucceeds() { every { save(any()) } just Runs }
fun Repo.verifySavedOnce(user: User) { verify(exactly = 1) { save(eq(user)) } }
// slot outside, capture inside
val saved = slot<User>()
every { repo.save(capture(saved)) } just Runsgo deeper
Follow the rules: nothing but the mock call inside every/verify, slots declared outside, no logging or setup in the block.
Explain why — the block is executed, possibly more than once, and matchers only exist inside the recorder's scope — and use MockKMatcherScope extensions for shared predicates.
Design the reuse layer: whole-statement helpers as the default, matcher extensions where a predicate recurs, all-matchers calls inside shared code, count-explicit verification helpers.
Weigh readability against reuse — set the threshold for extracting a helper, keep test-relevant arrangement inline, and name the failure mode you are designing against: side effects multiplied by an unspecified number of recording rounds.
## The two constraints **Recording is execution.** MockK builds a stub by running your lambda with a call recorder installed, and it may run that lambda more than once while it disambiguates which arguments were matchers. Any side effect inside the block therefore happens an unspecified number of times. **Matchers are scope-bound.** `any()`, `match { }`, `eq()`, `capture()` are members of `MockKMatcherScope` (verification blocks use `MockKVerificationScope`, which extends it). They are only in scope, and only meaningful, while that recorder is live. Every convention below is a consequence of those two facts. ## Reuse unit 1: scope-receiver matcher helpers ```kotlin fun MockKMatcherScope.anyActiveUser() = match<User> { it.active && it.tenantId == TEST_TENANT } ``` Declaring the receiver as `MockKMatcherScope` is what makes the helper callable from inside a block and uncallable anywhere else — the compiler enforces the scoping rule for you. This is the right unit when a domain predicate recurs across many stubs and deserves a name. Keep such helpers pure: they should return a matcher and nothing else. ## Reuse unit 2: whole-statement helpers ```kotlin fun Repo.stubFindReturns(user: User) { every { findById(any()) } returns user } fun Repo.verifySavedOnce(user: User) { verify(exactly = 1) { save(user) } } ``` This is the unit most suites should standardise on. The helper owns the entire statement, so callers never open a recording window themselves and cannot violate its rules. It also gives you one place to change when a signature changes, and it reads as domain vocabulary in the test body. The cost is indirection: keep the helper's name specific about *what it asserts*, otherwise a failure sends the reader hunting. ## What must not be inside a recording block - **Slot creation.** Declare `val slot = slot<Order>()` outside; `capture(slot)` inside. A slot created inside would be recreated on every recording round. - **Counters, list building, logging, IO.** Anything you would notice happening twice. - **Calling the system under test.** Exercise happens between arrange and verify, never inside a recording lambda. - **Branching and loops.** They can produce an empty recording (`Missing calls inside every { ... } block.`) or a variable number of matchers, and they defeat the one-call-per-block rule. - **Cross-thread work.** The recording window belongs to the block and the thread executing it. - **More than one mock call.** A block with several calls muddles which one the answer attaches to and makes failures harder to read. ## Judgement calls worth voicing - **How far to abstract.** A suite with a helper for every stub becomes a private DSL that new joiners must learn, and failures point at helper source rather than test source. A reasonable line: abstract when the same stub appears in three or more tests, or when the arrangement is genuinely incidental to what the test is about; otherwise let the `every` stand inline where the reader can see it. - **Fixtures vs helpers.** Prefer helpers that take domain objects and hide the MockK details over shared mutable fixture objects; the latter interact badly with recorded-call state and force everyone to reason about what a previous test left behind. - **All-matchers calls in shared helpers.** Because a helper is used in contexts you cannot see, write its calls with every argument as a matcher (`eq(...)` around constants). It removes matcher-binding inference as a source of failure inside code that is hard to debug from the call site. - **Keep verification helpers count-explicit.** A helper named `verifySaved` that hides a bare `verify { }` asserts only "at least once"; state the count in the helper so the name and the assertion agree. ## The failure you are designing against Without these conventions, the classic incident is a helper that mutates a shared list inside an `every` block and produces duplicate entries only on some machines or some MockK versions — because the number of recording rounds is not something your test controls. The whole discipline reduces to one sentence: **a recording block describes a call; it never does work.**
- Where do slots belong when you extract a capture into a helper?The slot is created outside the recording block and, if the helper owns the whole statement, the helper should create it and return it to the caller. A slot declared inside an every block would be re-created each time MockK re-runs the lambda, so the reference the test inspects afterwards is not reliably the one that was captured. Keeping slot lifetime in ordinary test code also makes it obvious when a slot outlives the test it belongs to.
- When is a shared stubbing helper the wrong call?When it hides the arrangement the test is actually about. If a test exists to prove behaviour under a specific collaborator response, that response should be visible in the test body. Helpers earn their place for incidental arrangement repeated across many tests — the boilerplate that has to be there for the object to work at all. The other warning sign is a helper whose name says less than the code it replaces, which turns every failure into a navigation exercise.
saying these in an interview costs you the question
- Assuming the every/verify lambda runs exactly once and putting counters or list building inside it
- Trying to build a matcher outside the block and pass it into a helper as a parameter
- Creating slots inside the recording block
- Wrapping several mock calls in one recording block for convenience
- Building a helper DSL so broad that failures point only at helper code and never at the test's intent