skip to content

Mocks, Spies and Lifecycle

How MockK creates its doubles — strict mocks, relaxed mocks, and spies over real objects — and how you keep them from leaking state between tests. Interviewers ask this to see whether you pick the right double deliberately and clean up after it.

on this pageshow

explore

questions

18

MockK can create a mock of a final Kotlin class without you marking it `open` or extracting an interface. Explain the instrumentation mechanism that makes that possible, and what it means for how the mock instance itself is produced.

level: middleimportance: must knowfreq 55%

answer

  1. agent rewrites bytecode, not a subclass
  2. final is irrelevant — hook is inside the method
  3. Objenesis: no constructor, no init block
  4. ByteBuddy on JVM, DexMaker on Android
  5. no call site → inline/static/constructor need other tools

basics

~20 s

MockK ships a JVM agent that rewrites class bytecode in place, inserting a dispatch hook into method bodies — so final classes and final methods are interceptable. The instance itself is allocated without running any constructor, then every call is routed to MockK's stub registry.

solid answer

~50 s

Kotlin classes and members are **final by default**, so a mocking library built on subclass proxies cannot override them — that is where the "make it `open` just for tests" smell comes from. MockK avoids proxies: `mockk<T>()` uses the MockK agent (ByteBuddy-based on the JVM, DexMaker on Android) to **transform the class's own bytecode**, adding a hook at the top of method bodies that asks MockK's dispatcher whether this call is being recorded or stubbed. Because the hook lives inside the real class, `final`, `internal` and `object` members are all interceptable. The instance is created with **Objenesis**-style allocation: no constructor, no `init` block, no property initializers run. Fields stay at JVM defaults, which is fine because every method call is intercepted before it can read them. Consequences: no `open` keyword or interface just for testability; but anything with no virtual call to hook — inline functions, top-level/static functions, constructors — needs a different tool (`mockkStatic`, `mockkConstructor`).

code

kotlin · 9 lines
kotlin
class PaymentGateway(private val http: HttpClient) {   // final by default
    init { http.warmUp() }                              // never runs for a mock
    fun charge(cents: Long): String = http.post("/charge", cents)
}

val gateway = mockk<PaymentGateway>()   // no `open`, no interface, no constructor call
every { gateway.charge(500) } returns "ok"

assertEquals("ok", gateway.charge(500))

go deeper

for a junior

Know the headline: MockK mocks final Kotlin classes without open or an extracted interface, unlike proxy-based mockers. Be able to say it uses a JVM agent.

for a middle

Explain the mechanism: agent-based bytecode instrumentation inserts a dispatch hook inside the real methods, so final is irrelevant, and the instance is allocated without running a constructor.

for a senior

Add the boundaries and operational detail — inline/static/constructor cases have no dispatch to hook, and the dynamic agent attach produces a JDK 21+ warning you should recognise in CI output.

for a principal

Frame it as design leverage: because testability no longer requires open or interface-per-collaborator, production APIs can stay final and intention-revealing; then say where you still prefer real objects over instrumentation-heavy tests.

## The problem MockK is solving In Kotlin, classes and their members are **final unless declared `open`**. A mocking library that works by generating a *subclass proxy* — a synthetic class that extends your type and overrides each method — is stuck: you cannot override a final method, and you cannot extend a final class. Java-first mocking tools grew up on that subclass technique, which is why teams reaching for them either extracted an interface for every collaborator or sprinkled `open` across production code purely for tests. Both are production-code changes made to satisfy a test tool. ## What MockK does instead MockK does not create a subclass. It ships an **agent** (`mockk-agent`, built on ByteBuddy on the JVM; `mockk-android` uses DexMaker on Android) that performs **inline instrumentation**: it takes the already-loaded class and rewrites its bytecode so that each method body begins with a call into MockK's dispatcher. The dispatcher asks: is a recording block (`every {}` / `verify {}`) currently active on this thread? Is this instance a mock? Is there a registered answer for this call signature? Because the hook is *inside the real class's own methods*, the `final` modifier is irrelevant — there is no override happening at all. The same trick is what lets MockK later mock Kotlin `object` singletons and companion members via `mockkObject`, and JVM statics via `mockkStatic`: those are all bytecode transformations of code that already exists, not new subclasses. On a modern JVM the agent is attached dynamically at test start. Since JDK 21 the JVM prints a warning when any agent is loaded dynamically; the usual answer is the `-XX:+EnableDynamicAgentLoading` flag on the test JVM, or attaching the agent at startup. It is a warning, not a failure, but it is worth recognising in CI logs. ## How the instance is produced Instrumentation handles *dispatch*; you still need an *object*. MockK allocates the mock **without invoking a constructor** (the Objenesis technique — the JVM can allocate an instance directly, bypassing `<init>`). That means: - constructors do not run, so a class whose constructor opens a socket, reads a file, or requires 12 dependencies is still trivially mockable; - `init` blocks and property initializers do not run; - backing fields are left at JVM defaults (`null`, `0`, `false`). Uninitialised fields are harmless for a pure mock, because no real method body ever executes — the hook returns MockK's answer and the original code after it never runs. It stops being harmless the moment real code *does* run on such an instance (e.g. `callOriginal()`), which is one reason spying over an already-constructed object is a different tool. ## What this buys you, and where it stops Buys you: - final classes, final methods, `data class`es, sealed subclasses — all mockable as-is; - no `open` keyword and no interface-per-collaborator in production code just for tests; - Kotlin-specific shapes like `object` singletons and companion functions become reachable (through the dedicated `mockkObject` / `mockkStatic` entry points). Stops at anything where **there is no method dispatch to intercept**: - **inline functions** — the compiler copies the body into the caller at compile time, so at runtime there is no call to your class to hook; - **top-level and `@JvmStatic` functions, and constructors** — not instance calls; they need `mockkStatic` / `mockkConstructor`, which are separate, explicitly scoped operations; - **JDK bootstrap classes** — instrumenting core platform classes is restricted and unreliable. ## Interview framing The answer an interviewer is listening for has three beats: (1) *no subclass proxy — the agent rewrites the class's own bytecode, so `final` does not matter*; (2) *the instance is allocated without running a constructor*; (3) *the technique needs a call site to intercept, so inline/static/constructor cases fall to the dedicated MockK entry points*. Candidates who answer only "MockK just supports final classes" have memorised a feature list, not a mechanism.

  • If MockK rewrites the class's own bytecode, why can't it mock an inline function?
    An inline function's body is copied into the call site by the Kotlin compiler at compile time. At runtime the caller never issues a call to your class for that function, so there is no dispatch for MockK's hook to intercept. Writing `every { obj.someInlineFun() }` records whatever the inlined body itself does, which is usually nothing mock-related, and MockK reports a missing-call error inside the block. The fix is to make the function non-inline, or to mock a non-inline collaborator it delegates to.
  • Does the fact that no constructor runs ever cause a problem?
    For a plain mock, no: every method is intercepted before the real body could read a field, so uninitialised fields are never observed. It matters when real code does execute on the instance — for example when an answer calls the original implementation — because a property initializer that would normally populate a field never ran, and the real body can hit a null. That is a reason to spy over a properly constructed instance instead of asking a mock to run real logic.

A subclass proxy is like wrapping a machine in a new casing with your own buttons — impossible if the casing is welded shut (final). MockK instead opens the machine and solders a switch onto each internal wire, so it does not matter how the casing is built.

saying these in an interview costs you the question

  • "MockK generates a subclass, it just handles final specially" — there is no subclass; the class's own bytecode is instrumented.
  • "You still need to make Kotlin classes open for MockK" — that requirement is exactly what MockK removes.
  • "mockk() calls the constructor, so my init logic runs" — it does not; the instance is allocated without a constructor.
  • "Because it instruments bytecode, MockK can mock literally anything" — inline functions, statics/top-level functions and constructors have no instance dispatch and need different entry points.
  • Claiming the JDK 21 dynamic-agent warning is a MockK bug or a test failure — it is a JVM warning about agent attachment.

context

open as a page

MockK's clearMocks takes flags such as answers, recordedCalls and childMocks. What does each one reset, and why would you ever clear only some of them?

level: middleimportance: must knowfreq 45%

basics

~20 s

answers drops the stubs from every; recordedCalls empties the interaction log verify reads; childMocks discards auto-created chained mocks. All default to true. Clearing selectively — e.g. answers = false — resets call counts while keeping stubs.

open as a page

A mock created with MockK's `mockk<T>(relaxed = true)` answers calls you never stubbed. What value does it actually return, per return type?

level: middleimportance: must knowfreq 45%

basics

~20 s

MockK generates a per-type default: Unit for Unit, 0 for numeric types, false for Boolean, an empty String, empty arrays — and for any other type, including your own domain classes, a brand-new mock of that type which is itself relaxed.

open as a page

In MockK, when you write spyk(existingInstance), what relationship does the resulting spy have with the instance you passed in — and what breaks if you assume it is a wrapper that delegates to that object?

level: middleimportance: must knowfreq 45%

basics

~20 s

spyk(obj) does not delegate. MockK builds a fresh instrumented instance of the same class and copies obj's fields into it, shallowly. Spy and original are then separate objects; state changes on one are invisible to the other.

open as a page

In MockK, what is the difference between clearAllMocks() and unmockkAll(), and which one do you need after a test has called mockkObject or mockkStatic?

level: seniorimportance: must knowfreq 45%

basics

~20 s

clearAllMocks wipes state — stubs, recorded calls, child mocks — but leaves the instrumentation in place, so an object or static stays patched. unmockkAll removes those registrations and restores original behaviour. After mockkObject/mockkStatic you need unmockkAll.

open as a page

When MockK builds an instance for `mockk<SomeClass>()`, are the class's constructor, `init` block and property initializers executed? Explain the consequences for the mock's internal state.

level: middleimportance: should knowfreq 28%

basics

~20 s

No. MockK allocates the instance without invoking any constructor, so init blocks and property initializers never run and backing fields stay at JVM defaults. It does not matter for a pure mock, because every method is intercepted before real code could read a field.

open as a page

MockK's `mockk()` accepts a `name` parameter, as in `mockk<Repository>(name = "primaryRepo")`. What does that name actually affect, and when is it worth setting?

level: middleimportance: should knowfreq 30%

basics

~20 s

It sets the mock's identity in its toString() and therefore in every MockK error and verification-failure message. Without it MockK prints a generated name like Repository(#1), which is ambiguous when a test holds several mocks of the same type.

open as a page

With a relaxed MockK mock, a chain like `service.repo().findAll()` runs without any stubbing at all. What object does `repo()` hand back, is it the same object on every call, and how do you verify calls made on it?

level: middleimportance: should knowfreq 30%

basics

~20 s

It hands back a child mock — a new mock of the return type, itself relaxed. MockK remembers child mocks, so the same call with the same arguments returns the same instance. To verify, capture that instance (val repo = service.repo()) and verify through the reference.

open as a page

MockK lets you write either spyk(MyService(...)) or spyk<MyService>(). How does the underlying instance come into existence in each case, and when does the second form blow up at runtime?

level: middleimportance: should knowfreq 32%

basics

~10 s

spyk(MyService(...)) spies a normally constructed object and copies its state. spyk<MyService>() creates the instance without running any constructor, so fields hold defaults — null, 0, false — and real methods that touch them fail.

open as a page

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%

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.

open as a page

A teammate claims that because MockK instruments bytecode it can mock anything. Where does a plain `mockk<T>()` actually stop — which kinds of functions cannot be intercepted that way, and why?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Interception needs a runtime call into the instrumented class. Inline functions have none — the compiler copies their body into the caller. Top-level, @JvmStatic and constructor calls are not instance calls, so they need MockK's dedicated mockkStatic / mockkConstructor entry points, and JDK bootstrap classes are effectively off limits.

open as a page

A MockK-based test passes on its own but fails when the whole class or suite runs, complaining about a stubbed value or a call count that belongs to a different test. What MockK state makes that possible, and how would you track it down?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Order dependence points at state that outlives a test: JVM-wide registrations from mockkObject/mockkStatic/mockkConstructor, and mocks held in shared or static fields. Bisect by running the suspects in sequence, then add unmockkAll teardown or scoped patches.

open as a page

A relaxed MockK mock backs a repository whose method returns a generic type parameter `T`, and the test dies with a `ClassCastException` at the call site rather than inside MockK. Why does relaxed mode struggle with generic return types, and what is the fix?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Type erasure: at runtime MockK only sees the erased return type (often Any), so it generates a child mock of that, and the compiler-inserted cast in your calling code fails. Fix it by stubbing the call explicitly with a real value of the expected type.

open as a page

You stubbed one call with `every` on a mock created with MockK's `relaxed = true`, and the code under test calls the same method with arguments your stub does not match. What happens, and why is that harder to diagnose than on a strict mock?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Nothing fails: the call misses your stub and falls through to the relaxed default — 0, empty string, or a child mock. A strict mock would have thrown no answer found for: naming the exact call. Relaxed mode converts a precise, immediate error into a silent wrong value.

open as a page

What does MockK's recordPrivateCalls = true parameter change about a spy, and once it is enabled, how do you stub or verify a private function?

level: seniorimportance: should knowfreq 25%

basics

~20 s

By default MockK ignores private members: they execute but are neither recorded nor stubbable. With recordPrivateCalls = true they go through MockK, and you reach them by name with the dynamic-call DSL: every/verify { spy invoke "name" withArguments listOf(...) }.

open as a page

How would you set the mock-isolation policy for a large Kotlin test suite that uses MockK — fresh mocks per test, clearMocks/clearAllMocks, or unmockkAll teardown — and what are the tradeoffs of each?

level: principalimportance: should knowfreq 24%

basics

~20 s

Default to creating mocks per test so no state can survive. Use clearMocks only for genuinely shared or mid-test resets. Make unmockkAll (or scoped mockk* blocks) mandatory for anything patched globally, in one enforced place.

open as a page

MockK's `mockk()` takes a `moreInterfaces` parameter. What problem does it solve, and how do you stub or verify a call that belongs to one of those extra interfaces?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

It makes one mock implement additional interfaces beyond the mocked type, so code that does an is/as check — such as closing a collaborator that happens to be AutoCloseable — takes the intended branch. To stub or verify those members you cast the mock to that interface.

open as a page

A Kotlin service suite leans heavily on MockK spies — spyk on production classes with one or two members stubbed, plus recordPrivateCalls used to verify internal helpers. As the person setting the policy, how would you decide what stays, and which MockK mechanics drive your reasoning?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Judge each spy by the mechanic it depends on: state copying, call-through executing real code, and string-named private-call verification. Keep spies at stable public seams; retire private-call assertions and constructor-free spies as refactors make injection possible.

open as a page