Code under test calls a mocked collaborator from several threads (or from parallel coroutines), and the MockK test captures those arguments to assert on them. What can go wrong with the captured data, and how would you design the test so the assertions are dependable?
answer
- slot = last-write-wins → nondeterministic under threads
- mutableListOf() is an ArrayList — not thread-safe
- finish first, then capture in verify
- verify(timeout = ...) instead of sleep
- assert sets/invariants, not interleavings
basics
~20 sA CapturingSlot keeps only a nondeterministic last value, and a plain ArrayList handed to capture in an every block is appended from many threads with no synchronisation. Let all work finish, then capture inside verify, and assert on the set rather than the order.
solid answer
~50 sTwo problems. A `CapturingSlot` is last-write-wins, so with concurrent calls "last" is whichever thread happened to arrive last — the assertion is a coin flip. And a plain `mutableListOf()` given to `capture` inside an `every` block is appended from every calling thread, with no synchronisation of its own; you can lose elements or see stale contents. The design that holds up: **make the concurrency finish first, then capture.** Join the threads, await the coroutines, or use `verify(timeout = ...)` to wait for the expected count. Then capture in a `verify` block — verification re-matches the calls MockK already recorded, on the verifying thread, so the appends are single-threaded. Then assert on the right thing. Order across threads is not a property of the system, so compare sets or sorted lists, not sequences; verifying order across concurrent calls encodes a race as an expectation. If you must capture during the call, use a thread-safe collection.
code
kotlin · 12 lines// RACY: appended from N pool threads into a plain ArrayList
val seen = mutableListOf<Task>()
every { worker.submit(capture(seen)) } just Runs
// DEPENDABLE: let the work finish, then capture during verification
val submitted = mutableListOf<Task>()
pool.invokeAll(jobs)
pool.shutdown()
pool.awaitTermination(5, TimeUnit.SECONDS)
verify(exactly = jobs.size) { worker.submit(capture(submitted)) }
assertEquals(expectedTasks.toSet(), submitted.toSet()) // order is not a propertygo deeper
Recognise that a slot only keeps one value, so concurrent calls make it unpredictable; wait for the work to finish before asserting.
Explain that a plain list is not thread-safe, capture in verify after completion, and assert on the set rather than the order.
Design the whole test: real completion signal or verify(timeout), single-threaded capture during verification, invariant-shaped assertions, thread-safe collection if capture during the call is unavoidable.
Argue about instrument choice — when argument capture is worth it at all under concurrency versus asserting outcomes and invariants — and set the suite policy that keeps these tests from becoming a permanent flake budget.
## Why captures and concurrency mix badly Capture is a matcher side effect: it writes into a holder you own, at the moment a matcher is evaluated against an invocation. When those invocations arrive from several threads, the writing happens on those threads too — and the holders MockK gives you are ordinary Kotlin objects with no concurrency guarantees of their own. **A `CapturingSlot` is last-write-wins.** With sequential calls "last" is meaningful. With concurrent calls it is whichever thread got there last this run, so any assertion on `slot.captured` is a coin flip that will pass on your machine and fail in CI. **A `MutableList` from `mutableListOf()` is an `ArrayList`.** Appending to it from several threads is unsafe: you can lose elements, and the asserting thread has no guarantee of seeing the writes at all unless something establishes a happens-before edge. So the size assertion becomes flaky in both directions. ## The structural fix: finish, then capture Capturing inside `verify` is qualitatively different from capturing inside `every`. Verification does not re-run production code; it re-matches the invocations MockK has already recorded, and it does so on the thread running the verify block. If all the concurrent work has genuinely completed before you verify, the appends into your list happen on one thread, in the order MockK recorded the calls. So the test shape is: 1. Kick off the concurrent work. 2. **Wait for it properly** — `join()` the threads, `awaitTermination` the pool, `awaitAll` the deferreds, or await the coroutine scope. If the code under test gives you no completion handle, `verify(timeout = ...) { collaborator.submit(any()) }` polls until the expected calls have been recorded, which is a real synchronisation point rather than a guess. 3. **Then** capture: `verify(exactly = n) { collaborator.submit(capture(seen)) }`. What you must not do is `Thread.sleep` and hope. A sleep tuned on a laptop is a timeout on a loaded CI runner and a masked bug when the code is slower than expected. ## Assert on the right property Even with a correct capture, the *order* in which concurrent calls arrive is usually not a property of the system under test — it is scheduling noise. Asserting a sequence encodes the race as an expectation and produces a test that fails for no defect. Compare sets (`seen.toSet()`), sorted projections (`seen.map { it.id }.sorted()`), or invariants ("every task was submitted exactly once", "no id appeared twice"). Ordering verification modes belong to interactions that are genuinely ordered — typically a single-threaded sequence within one worker. ## When you truly must capture during the call Some tests need the argument while the call is in flight — for instance the stub's answer depends on it, or you want to release a latch when the nth call arrives. In that case do not hand `capture` a bare `ArrayList`. Use a concurrent collection (`ConcurrentLinkedQueue`, a synchronised list) populated from an `answers { }` block, and take your happens-before from the same construct that ends the test (latch, join, `awaitTermination`) before you read it. ## Zooming out: should the assertion be about arguments at all? At this level the better question is often whether a capture-based assertion is the right instrument. Concurrency tests are most robust when they assert on observable outcomes and invariants — final state, counts, absence of duplicates or lost work — rather than on the exact arguments of interleaved interactions. Capture earns its place when the identity of the work matters ("all 100 tasks were submitted, none twice") and the assertion is expressible as a set property. If the only way to express the expectation is an interleaving, the test is asserting the scheduler, and it will pay for that forever. ## Checklist - No slot for concurrent calls; a collection or nothing. - Completion is established by a real synchronisation point, never a sleep. - Capture in `verify`, after completion, so the appends are single-threaded. - Assertions are set- or invariant-shaped, not order-shaped. - If capturing during the call is unavoidable, the collection is thread-safe and read after a happens-before edge.
- Why is capturing inside verify safer than capturing inside every when calls come from many threads?Verification does not re-run production code; it re-matches the invocations MockK already recorded, and it does that on the thread executing the verify block. So the appends into your list are single-threaded and complete, provided all the concurrent work finished first. Capturing inside `every` puts the appends on the calling threads, where an unsynchronised list can lose elements.
- The code under test gives you no handle to wait on. How do you avoid a sleep?Use `verify(timeout = ...)` for the expected interaction — it polls the recorded calls until the expectation is satisfied or the timeout expires, which fails fast when the call never comes and returns as soon as it does. That gives a real synchronisation point tied to the behaviour you care about, rather than a fixed delay that is simultaneously too long locally and too short on a loaded CI machine.
- When would you drop the argument assertions entirely?When the only way to express the expectation involves an interleaving, the test is asserting the scheduler rather than the system. In that case assert observable outcomes instead — final state, total counts, no duplicates, no lost work — and keep interaction assertions for the parts of the flow that are genuinely sequential.
saying these in an interview costs you the question
- Using a CapturingSlot for concurrent calls and treating the last value as deterministic
- Handing a plain mutableListOf() to capture inside an every block that fires from many threads
- Adding Thread.sleep before the assertions instead of a real synchronisation point
- Verifying call order across threads and calling the resulting flakiness a MockK bug
- Assuming clearAllMocks or a fresh mock per test fixes what is actually a data race in the test's own collection