A MockK test captures a collaborator's argument with a CapturingSlot, but the mocked method is invoked several times during the test. Which value does the slot hold when the test asserts, and how would you keep every call's argument instead?
answer
- capture = any() with a side effect
- slot = one value, last write wins
- capture(mutableListOf()) keeps them all, in order
- isCaptured / isNull guard the read
- captureNullable for nullable params
basics
~20 sA CapturingSlot holds one value: each matching call overwrites it, so you end up with the last call's argument. To keep them all, capture into a MutableList — capture(list) appends every matching call's argument in call order.
solid answer
~50 s`slot<T>()` creates a `CapturingSlot<T>`, a single-value holder. `capture(slot)` is a matcher that accepts anything (like `any()`) and, as a side effect, writes the actual argument into the slot. Because it is one holder, every matching invocation overwrites it — after three calls `slot.captured` is the third argument, not the first, and there is no history. When the collaborator is called more than once, use the `MutableList` overload: declare `val args = mutableListOf<Order>()` and write `verify(exactly = 2) { gateway.send(capture(args)) }`. MockK appends each matching call's argument, so the list gives you both the values and their order. Two details worth stating: `slot.isCaptured` tells you whether anything ever matched (a slot that never matched has nothing useful in `captured`), and for a parameter that can legitimately be `null` you use `captureNullable`, with `slot.isNull` reporting that a null arrived.
code
kotlin · 11 linesval repo = mockk<UserRepo>(relaxUnitFun = true)
val last = slot<User>()
val all = mutableListOf<User>()
service.importAll(listOf(alice, bob))
verify(exactly = 2) { repo.save(capture(all)) }
verify { repo.save(capture(last)) }
assertEquals(listOf(alice, bob), all) // every call, in order
assertEquals(bob, last.captured) // only the last onego deeper
Be able to say a slot holds one value — the last matching call — and that a MutableList captures them all. Show the two-line code shape.
Explain capture as a matcher with a side effect, use the list overload with an explicit verify(exactly = n), and mention isCaptured before reading captured.
Add lifetime and hygiene: slots are test state that mock-clearing does not touch, captures fire wherever matching happens, and prefer match {} over capture when the assertion is simple.
Frame it as a suite policy: when captures buy better diagnostics versus when they couple tests to argument construction, and how you keep capture state per-test so parallel or ordered runs stay deterministic.
## The two pieces An argument capture in MockK is built from two halves. `slot<T>()` creates a `CapturingSlot<T>` — a small mutable holder that owns one value plus the flags `isCaptured` and `isNull`, and a `clear()` method. `capture(slot)` is what you write inside an `every { }` or `verify { }` block, in the position of the argument you care about. The key idea is that `capture(...)` is an argument *matcher* with a side effect. For matching purposes it behaves like `any()`: it accepts whatever value is passed in that position. While it matches, it writes the real argument into the holder you handed it. Almost every capture question follows from that one sentence. ## One slot holds exactly one value Each time a call matches, the argument is written into the slot, replacing whatever was there. After three matching invocations, `slot.captured` is the argument of the third. There is no index, no history, no "first" accessor. The most common bug in real test suites is a test that stubs once, exercises a loop or a batch, then asserts `slot.captured == firstExpected` and is confused when it sees the last item. ## Capturing every call: the MutableList overload `capture` is overloaded to accept a `MutableList<T>`. Give it a list and MockK appends each matching call's argument rather than overwriting: - The list is populated in the order the matching calls are processed, which for a `verify` block is the order MockK recorded them — i.e. real call order. - Asserting on the whole list (`assertEquals(listOf(alice, bob), captured)`) asserts values *and* order in one step. - The list's size incidentally tells you the call count, but `verify(exactly = n)` states that intent far more clearly and produces a better failure message. Use both: the count assertion in the verify, the content assertion on the list. ## Nullable parameters `capture` is declared for non-null types. For a parameter that may legitimately receive `null`, MockK provides `captureNullable`, which works with a `CapturingSlot<T?>` or a `MutableList<T?>`. The slot's `isNull` flag then reports that a null was the captured value, while `isCaptured` reports whether anything arrived at all. Guard on `isCaptured` before touching `captured`: a slot that never matched has nothing meaningful to give you, and reading it is a confusing way to fail. ## Lifetime: slots are your state, not the mock's A slot or list is ordinary test state. Declared inside the test function it is fresh for every test. Hoisted to a class property and reused across tests, it silently carries values from the previous test into the next one — a classic flaky-test source when tests run in a shared class instance. Note that `clearMocks` / `clearAllMocks` reset the *mock's* answers and recorded calls; they know nothing about your list or slot, so those keep whatever they accumulated. `CapturingSlot.clear()` exists for the case where you deliberately reuse one within a single test. ## Where the capture actually fires Because capture is a side effect of matching, it fires wherever matching happens. Put `capture(list)` in an `every` block and it fires as the production code calls the mock. Put it in a `verify` block and it fires while MockK re-matches the recorded calls during verification. Hand the *same* list to both, or run the same verify twice, and the same arguments land in it twice — a surprising duplicate count that has nothing to do with how many times production called the mock. ## The shape that stays readable A capture assertion that ages well looks like this: capture in `verify`, one capture target per assertion, count asserted with `verify(exactly = n)`, content asserted against the captured list. If you only care that *some* call carried a particular value, a matcher (`verify { gateway.send(match { it.id == 42 }) }`) is usually clearer than capturing and then hand-searching the list — capture earns its place when you want the failure message to show you the actual object, or when the assertion is too rich to express as a matcher.
- What does MockK's CapturingSlot give you when nothing ever matched, and how should a test protect itself?The slot is simply never written, and `isCaptured` stays false; reading `captured` at that point is meaningless and fails in an unhelpful way. Check `isCaptured`, or better, assert the interaction first with `verify(exactly = 1) { ... }` so the failure message names the missing call rather than the empty slot.
- How do you capture an argument for a parameter that can legitimately be null?Use `captureNullable` instead of `capture`, backed by a `CapturingSlot<T?>` or `MutableList<T?>`, because `capture` is declared for non-null types. The slot then exposes `isNull` to tell you a null was captured, distinguished from `isCaptured` which only says whether any matching call happened at all.
- If you capture into a list declared as a test-class property, what goes wrong?The list is your own state, not the mock's, so `clearMocks` and `unmockkAll` do not touch it and values from the previous test survive into the next one. Assertions on size then fail intermittently depending on test order. Declare the list inside the test function, or clear it in an @BeforeEach alongside the mock reset.
saying these in an interview costs you the question
- Believing a single slot accumulates all calls and that `captured` is a collection
- Asserting `slot.captured` expecting the *first* call's argument after a loop
- Thinking `clearMocks`/`clearAllMocks` also empties your captured list or slot
- Claiming capture only works inside `verify` and cannot appear in `every`
- Using the captured list's size as the primary call-count assertion instead of `verify(exactly = n)`