skip to content

Returns, Throws and Chaining

The core stubbing verbs and how successive calls can yield different results. Interviewers use returnsMany/andThen to check you can simulate retry, pagination, and failure-then-recovery scenarios.

on this pageshow

explore

questions

5

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?

level: middleimportance: must knowfreq 55%

answer

  1. andThen / returnsMany = one stub, list of answers
  2. counter per stub, advanced only by matching calls
  3. exhausted → last value repeats forever
  4. never wraps, never auto-throws
  5. re-stub replaces the chain and resets position

basics

~20 s

Chain 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 s

MockK 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 lines
kotlin
val 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 repeats

go deeper

for a junior

Know both spellings — returnsMany and returns … andThen … — and be able to state that the last value repeats after the list is used up.

for a middle

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.

for a senior

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.

for a principal

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.

context

open as a page

In a MockK test you write `every { repo.find(any()) } returns A` and later, in the same test, `every { repo.find(1) } returns B`. Which stub wins, and does re-stubbing reset an existing answer chain or the mock's recorded calls?

level: seniorimportance: must knowfreq 40%

basics

~20 s

The most recently registered matching stub wins, so find(1) returns B and everything else returns A. Declaration order decides, not how specific the matcher is. Re-stubbing a call installs a new answer, so any chain in the old one is abandoned and sequencing starts over — but recorded calls are untouched.

open as a page

You mocked a collaborator with MockK and the code under test calls one of its Unit-returning methods. Why does that call still fail on a non-relaxed mock, and what exactly does `just Runs` do about it?

level: middleimportance: should knowfreq 45%

basics

~20 s

A plain mock has no answer for any call, including Unit ones, so it throws a MockKException. every { x.log(any()) } just Runs registers the answer "return Unit and do nothing". justRun { x.log(any()) } is the same thing in one call.

open as a page

Using MockK, how would you stub an HTTP client so that the first two calls throw an IOException and the third returns a response — and what should you be careful about when reusing one exception instance across calls?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Mix terminators in one chain: every { client.get(any()) } throws IOException("boom") andThenThrows IOException("boom") andThen response. Each entry is a separate answer. The same Throwable instance is rethrown every time it is used, so its stack trace stays from where you built it and any mutation on it is shared.

open as a page

A test needs a collaborator that fails on the first two attempts and succeeds afterwards. Compare encoding that as a MockK `andThen` chain against re-stubbing the same call between phases of the test — how do you choose, and what breaks later?

level: principalimportance: should knowfreq 25%

basics

~20 s

A chain ties behavior to call position, so it is compact but breaks whenever the number of calls changes. Re-stubbing ties behavior to a point in the test's narrative, so it survives extra calls but only works if the test can pause between phases. Choose by whether the test's story is about counting calls or about a state change.

open as a page