skip to content

On a MockK spy, does writing every { spy.loadConfig() } returns fake actually execute the real loadConfig() while the stub is being set up? Explain what MockK does inside every/verify blocks versus during the test body.

level: middleimportance: should knowfreq 35%

answer

  1. every/verify = recording mode
  2. recording intercepts, does not execute
  3. setup calls are not counted
  4. body: match → stub, no match → real code
  5. argument mismatch silently calls through

basics

~20 s

No. Inside every/verify, MockK is in recording mode: the call is intercepted to capture its signature, not executed, and it is not counted as an interaction. Real bodies only run in the test body, for members you did not stub.

solid answer

~50 s

MockK's double has two modes. **Recording mode** — inside an `every { }` or `verify { }` block. Calls on the spy are intercepted purely to capture the signature and argument matchers. The real implementation is **not** invoked, and the call is **not** added to the recorded-interaction log, so it never inflates a later count. **Answering mode** — the test body. A call that matches a recorded stub gets the stubbed answer and the real body is skipped. A call that matches nothing runs the spy's real implementation (that is what makes it a spy rather than a mock). Either way the call is recorded, so `verify` can assert on it. That is why selective stubbing on a spy is safe: stubbing `loadConfig()` costs nothing at setup time and leaves every other member real. One caution: MockK does not guarantee a recording lambda is evaluated exactly once, so keep side-effecting expressions out of `every { }` blocks — only the call you want to match belongs there.

code

kotlin · 11 lines
kotlin
class PricingService(private val rates: Rates) {
    fun total(o: Order) = o.amount * remoteRate(o.currency)
    fun remoteRate(cur: String): Double = httpFetchRate(cur)
}

val service = spyk(PricingService(rates))
every { service.remoteRate("EUR") } returns 1.1  // no HTTP call during setup

val total = service.total(Order(100.0, "EUR"))   // real total(), stubbed rate

verify(exactly = 1) { service.remoteRate("EUR") } // the setup call is not counted

go deeper

for a junior

Know that setting up a stub does not run the real method, and that anything you did not stub on a spy still runs for real.

for a middle

Describe the two modes explicitly — recording (intercept, do not execute, do not count) versus answering (match → stub, else call through) — and why selective stubbing is safe.

for a senior

Use the model diagnostically: explain an unexpected side effect or an off-by-one verify count in terms of which mode the call happened in and whether the matchers matched.

for a principal

Frame it as the boundary of what a spy can safely express, and set expectations about how much stubbing on a spy is acceptable before the test stops testing the real object.

## Two modes, one object A MockK double behaves differently depending on where the call happens. ### Recording mode: inside every { } and verify { } When you open an `every { }` block, MockK puts its gateway into a recording state. Every call made on a double inside that lambda is grabbed by the interceptor, which extracts the target, the method signature and the argument matchers, and stores that as a *call pattern*. The interceptor returns a placeholder value so the block can finish evaluating. Two consequences follow, and they are the interview point: 1. **The real body does not run.** For a spy this matters enormously — stubbing `every { spy.chargeCard(any()) } returns Ok` does not charge a card while you set the stub up. 2. **Nothing is counted.** The call you made inside the block is a pattern, not an interaction, so a later `verify(exactly = 1)` is not thrown off by the fact that the same method appeared in the setup block. `verify { }` works the same way in reverse: the calls you write there are patterns matched against the recorded log, not new invocations. ### Answering mode: the test body Outside those blocks, a call on the spy is matched against the recorded patterns: - **Match found** → MockK returns the configured answer and the real implementation is skipped entirely. - **No match** → the spy calls through to the real implementation and returns whatever it produces. (A plain, non-relaxed mock would instead fail here — call-through is precisely the spy's defining behaviour.) In both cases the invocation is appended to the recorded-call log, which is what `verify` later inspects. So a spy gives you real results *and* a full interaction record for the same call. ## Why this makes selective stubbing work "Selective stubbing" means: take a real object, replace one member, keep the rest real. The mode split is what makes it practical. ```kotlin val service = spyk(PricingService(rates)) every { service.remoteRate("EUR") } returns 1.1 // no network call happens here val total = service.total(order) // real total(), which internally uses the stub path verify { service.remoteRate("EUR") } ``` Had the recording block executed the real `remoteRate`, this pattern would be unusable for anything with side effects — you would have to perform the very call you were trying to avoid in order to stub it away. ## Matching is by pattern, not by identity The recorded pattern includes the argument matchers you used. `every { spy.rate("EUR") }` only intercepts calls with `"EUR"`; `spy.rate("USD")` still calls through to real code. That is how one object can be part stub, part real, and it is often surprising: a test that stubs a narrow argument and then passes a different one will silently execute production code. ## Practical cautions - **Keep side effects out of recording blocks.** MockK does not promise the lambda is evaluated exactly once, and the block is there to describe a call, not to do work. Compute values before the block and reference them inside it. - **Stub what you must, not what you can.** The more members you stub on a spy, the less of the real object you are testing; past a threshold you want a mock. - **Reverting to real behaviour** for a member you previously stubbed is done with an answer that calls the original implementation rather than by "unstubbing" the method. - **Chained/nested calls inside a recording block** are how MockK builds child mocks for deep stubbing; that is a different mechanism from call-through and worth keeping distinct in your head. ## Diagnosing from symptoms - *Real code ran when I thought I had stubbed it* → the argument matchers in your `every` did not match the actual call; the spy fell through to the real body. - *A verify count is one higher than expected* → the extra call is a genuine invocation in the test body, not your setup block; setup calls are never counted. - *A side effect happened during setup* → something in the recording lambda other than the intercepted call itself performed it.

  • On the same spy, what happens to a call whose arguments do not match any stub you recorded?
    It falls through to the real implementation and returns the real result, because call-through is the spy's default answer. It is still recorded, so verify can see it. This is a common source of surprise: a narrowly matched stub leaves every other argument value executing production code.
  • Why is it bad practice to put side-effecting statements inside an every { } block?
    The block exists only to describe a call pattern, and MockK gives no guarantee about how many times it evaluates the lambda. Anything with an observable effect placed there may run an unexpected number of times or at an unexpected point. Compute values beforehand and reference them inside the block.

saying these in an interview costs you the question

  • Believing every { spy.foo() } executes the real foo() during setup
  • Expecting calls written inside every/verify blocks to count toward verification counts
  • Assuming stubbing one member on a spy disables the rest of the class
  • Not realising that an argument-matcher mismatch silently runs real production code
  • Putting assertions or side effects inside a recording lambda

context