In MockK, how do you make one stubbed function return a different value on each successive call, and what does that stub return once the list of values is used up?
answer
- andThen / returnsMany = one stub, list of answers
- counter per stub, advanced only by matching calls
- exhausted → last value repeats forever
- never wraps, never auto-throws
- re-stub replaces the chain and resets position
basics
~20 sChain answers: every { c.next() } returns 1 andThen 2 andThen 3, or returnsMany listOf(1, 2, 3). MockK hands out one entry per matching call, in order. When the list runs out it repeats the last entry forever — it does not throw and does not restart.
solid answer
~50 sMockK lets a single stub hold a **sequence of answers**. Two equivalent spellings: ```kotlin every { client.fetch() } returns "a" andThen "b" andThen "c" every { client.fetch() } returnsMany listOf("a", "b", "c") ``` Internally the sequence is one answer object holding a list plus a call counter. Each *matching* invocation increments the counter and returns the entry at that index. The counter is per stub, not per test. The part people get wrong is exhaustion: after the last entry, MockK **keeps returning the last entry** for every further call. A four-element loop over a three-element chain sees `a, b, c, c` — no exception, no wrap-around. If you want the fourth call to blow up you must say so explicitly, e.g. `andThenThrows`. Only calls that match the recorded matchers advance the counter, so `fetch(1)` and `fetch(2)` stubbed separately keep independent counters. Re-stubbing the same call replaces the sequence and resets the counter.
code
kotlin · 10 linesval client = mockk<TokenClient>()
every { client.fetch() } returns "a" andThen "b" andThen "c"
// identical:
// every { client.fetch() } returnsMany listOf("a", "b", "c")
client.fetch() // "a"
client.fetch() // "b"
client.fetch() // "c"
client.fetch() // "c" <- last answer repeatsgo deeper
Know both spellings — returnsMany and returns … andThen … — and be able to state that the last value repeats after the list is used up.
Explain the mechanism: one stub holds an ordered list plus a counter, advanced only by calls matching that stub's matchers, clamped to the last entry.
Add the operational consequences: chains encode call order, so they break on innocuous refactors; call counts belong in verifications; use andThenThrows when an extra call must fail.
Frame it as a design choice — positional sequencing versus matcher-driven or state-driven doubles — and set a team norm about when a chain has become an unreadable script.
## The problem A fixed stub answers the same thing forever. Plenty of real collaborators do not: a paging API returns a page then an empty page, a token client returns a stale token then a fresh one, a queue returns an item then `null`. To test the code that loops or retries around such a collaborator you need a stub whose answer *evolves across calls*. ## The two spellings MockK's `every { }` returns a stub scope, and every terminator on it (`returns`, `throws`, `answers { }`) registers an **answer object** for the recorded call. Two terminators register a *multi* answer: - `returnsMany listOf(a, b, c)` — a list up front. There is also a vararg form, `returnsMany(a, b, c)`. - `returns a andThen b andThen c` — the first terminator registers one answer and hands back an *additional answer scope*, whose `andThen` appends further entries. Siblings on that scope are `andThenMany(list)`, `andThenThrows(ex)`, `andThenThrowsMany(list)` and `andThenAnswer { }`. Both end up as the same thing: one registered answer holding an ordered list plus an internal counter. ## Dispatch semantics When the mocked function is called, MockK finds the matching recorded stub and asks its answer object for a value. The multi-answer implementation increments its counter and picks `list[min(index, list.size - 1)]`. Three consequences follow directly: 1. **Exhaustion repeats the last entry.** Call four times against a three-entry chain and you get `a, b, c, c`. Candidates routinely assume the fourth call throws "no answer found" or wraps back to `a`; neither happens. If the test's real assertion is "the code only calls this three times", express that with a verification, not by hoping the stub fails. 2. **The counter is per stub, and it is shared across the whole test.** Two different tests get fresh mocks (or a cleared mock), so they start at zero; two calls from two different points in the *same* test share one counter. That is exactly what makes chains work for retry loops — and exactly what makes them fragile when unrelated code paths also touch the mock. 3. **Only matching calls advance it.** The counter belongs to the stub, and the stub is selected by its argument matchers. `every { repo.find(1) } returns A andThen B` and `every { repo.find(2) } returns C` are independent; calling `find(2)` does not consume an entry from the `find(1)` chain. ## Interaction with re-stubbing Registering a new `every` for the same call adds a newer answer that takes precedence, so the old chain — including its position — is effectively abandoned and the new sequence starts at its first entry. Clearing the mock's answers (`clearMocks(mock)`) removes the stub entirely, after which the call is unstubbed again and a non-relaxed mock will fail with a MockK exception rather than replay the old chain. ## Where chains earn their keep The canonical uses are: - **Retry/backoff code**: fail, fail, succeed. - **Polling loops**: `false, false, true`. - **Pagination**: full page, full page, empty page — and here the exhaustion rule is a feature, because the terminal empty page repeating forever is the correct behavior for a well-written loop and turns an off-by-one bug into a hang you notice rather than a confusing exception. ## Where they hurt A chain encodes *call order and call count* into the stub. Any refactor that adds a defensive extra call — a log line that reads the same getter, a metric, a null check — silently shifts every later answer by one and the test fails somewhere far from the change. When the collaborator's behavior depends on the *argument* rather than the *position*, use argument matchers or a computed answer instead of a positional chain; when it depends on accumulated state, a small hand-written fake is more honest than a five-link chain. ## Suspend functions The same terminators are available after `coEvery { }`, so a suspend collaborator chains identically; nothing about the sequencing model changes. ## Quick checklist - Sequence = one stub, ordered list, internal counter. - Exhausted ⇒ last entry repeats; never wraps, never throws by itself. - Counters are per-stub and matcher-scoped. - Re-stubbing replaces the sequence and starts over.
- Your production loop calls the stubbed method four times but the chain has three entries and the test still passes. Is that a problem?It is a smell worth checking. The fourth call silently reuses the third answer, so the test says nothing about whether four calls were intended. If the call count matters, assert it explicitly with a verification such as verify(exactly = 3) rather than relying on the chain to run out, because the chain will never complain.
- How would you make the call after the sequence fail instead of repeating the last value?End the chain with an explicit failing step, for example `returns "a" andThen "b" andThenThrows IllegalStateException("unexpected extra call")`. That converts the fourth invocation into a loud failure. It is still a sequence, so the exception itself will then repeat for any fifth or later call.
- Do these terminators work the same on a suspend function?Yes. Record the call with coEvery instead of every and the same returns/returnsMany/andThen chain applies; the multi-answer object and its counter are identical. Only the recording function differs because the stubbed call must be made from a suspending context.
A vending machine loaded with three cans and a jammed dispenser: it gives can 1, can 2, can 3, and then keeps handing you copies of can 3 — it does not refuse service and it does not go back to can 1.
saying these in an interview costs you the question
- Believing the sequence wraps around to the first value once exhausted.
- Believing an exhausted chain throws "no answer found" and using that as an implicit call-count assertion.
- Thinking the counter resets between assertions or after a verify inside the same test.
- Assuming every call on the mock advances the chain, even calls that match a different stub.
- Writing long positional chains to model behavior that really depends on the arguments.