skip to content

What does spyk() create in MockK, and how does its behavior for unstubbed methods differ from a relaxed mock?

level: middleimportance: must knowfreq 60%

answer

  1. Spy wraps a REAL object
  2. Unstubbed -> real code runs
  3. Relaxed mock -> fake default, never real
  4. spyk(obj) vs spyk<T>()
  5. Self-calls can bypass stubs

basics

~10 s

spyk() wraps a real object. Unstubbed methods run the real code and return real results. A relaxed mock never runs real code; unstubbed methods just return fake defaults.

solid answer

~50 s

spyk() creates a spy: a partial mock that wraps a real instance (either spyk(realObject) or spyk<T>() which constructs one). For any method you do NOT stub, the spy calls through to the real implementation and returns its real result; methods you DO stub via every { } are overridden. Contrast with mockk(relaxed = true): a relaxed mock never executes real logic — unstubbed methods return synthesized defaults (0, "", empty, nested mocks). So spy = real-by-default + selective fakes; relaxed mock = fake-by-default. Spies are useful when you want most of a real collaborator's behavior but need to stub one expensive or nondeterministic call, or to verify { } that a real method was invoked. Watch out: with spyk(realObject) the spy wraps a copy/proxy, and internal self-calls may bypass the stub depending on dispatch; final classes need MockK's inline mock-maker.

code

kotlin · 12 lines
kotlin
class Greeter {
    fun name() = "world"
    fun greet() = "hello, " + name()
}

val spy = spyk<Greeter>()
println(spy.greet())            // "hello, world" (real)

every { spy.name() } returns "mockk"
println(spy.name())             // "mockk" (stubbed)
// NOTE: greet()'s internal call to name() may still hit the
// real name() depending on dispatch -> classic spy gotcha

go deeper

for a junior

Knows a spy wraps a real object and runs real methods unless stubbed.

for a middle

Clearly contrasts spy (real-by-default) vs relaxed mock (fake-by-default) and uses spyk to neutralize one call.

for a senior

Explains the self-call/dispatch gotcha and final-class instrumentation; chooses spies judiciously over full mocks.

for a principal

Guides when spies are a smell (testing implementation detail) vs. legitimate, and the maintainability cost of internal-call assumptions.

## Spy = partial mock over a real object A **spy** wraps a concrete instance. Calls go to the real method **unless** you've stubbed them. Two ways to create one: ```kotlin val spy1 = spyk(Calculator()) // wrap an existing/real instance val spy2 = spyk<Calculator>() // MockK constructs a real instance for you ``` ## Call-through behavior ```kotlin class Calculator { fun add(a: Int, b: Int) = a + b fun expensive(): Int = slowNetwork() } val spy = spyk<Calculator>() spy.add(2, 3) // 5 <- REAL method runs every { spy.expensive() } returns 0 // override the costly one spy.expensive() // 0 <- stub wins ``` Unstubbed `add` executes real code; stubbed `expensive` returns the fake. ## Versus a relaxed mock | | `spyk<T>()` | `mockk<T>(relaxed = true)` | |---|---|---| | Unstubbed method | runs **real** code, real result | returns **synthetic default** | | Stubbed method | uses your stub | uses your stub | | Real object needed | yes (wraps one) | no (pure fake) | | Use when | mostly-real, stub a few | mostly-fake, stub a few | So the axis is *default behavior*: spy defaults to **real**, relaxed mock defaults to **fake**. Plain `mockk()` defaults to **throw**. ## Why spies are handy - Keep genuine business logic but neutralize one side effect (network, clock, randomness). - `verify { spy.helper(any()) }` to assert a real internal helper was called. ## Gotchas - **Self-calls / internal dispatch**: when a real method calls another method on `this`, that inner call may go to the real implementation and **bypass your stub**, because the real object's `this` isn't always the spy proxy. Don't rely on stubbing internal calls. - **Final classes/methods**: Kotlin classes are final by default; MockK's inline mock-maker (bytecode instrumentation) handles them, but be aware it's doing more than a simple proxy. - **State**: with `spyk(realObj)`, mutations may apply to the wrapped instance — reason about shared state. ## Combine with relaxation `spyk<T>(recordPrivateCalls = true)` and relaxation flags can be combined, but the core mental model stays: spy calls through by default.

  • If you stub greet's internal call to name(), why might greet() still print the real name?
    The real greet() invokes this.name() on the underlying real object, whose this isn't the spy proxy, so the stub can be bypassed. Spies don't reliably intercept self-calls.
  • When would you pick spyk over a real object plus a real dependency?
    When you want almost all real behavior but must neutralize one nondeterministic/expensive call, or verify an internal method was invoked without rewriting the class.

A spy is a stunt double who does the real stunts except the dangerous one you replace; a relaxed mock is a cardboard cutout that just stands in for everything.

saying these in an interview costs you the question

  • Saying spyk never runs real code (that's a mock)
  • Claiming relaxed mocks call through to real implementations
  • Assuming stubbing always intercepts internal self-calls on a spy
  • Thinking spyk requires the class to be open (MockK inline mock-maker handles final)

context