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?
answer
- needs a runtime call, on an instrumented class, on a mock instance
- inline/reified = compiled into the caller → nothing to hook
- static/top-level/extension → mockkStatic (global, scoped)
- constructors → mockkConstructor; JDK bootstrap restricted
- limits map to seams: inject instead of patch
basics
~20 sInterception 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.
solid answer
~60 sMockK's inline instrumentation removes the *`final`* limitation, not every limitation. The rule is: **there must be a call at runtime, into an instrumented class, on a mock instance.** What falls outside: - **`inline` functions** — the Kotlin compiler copies the body into the call site at compile time, so at runtime nothing calls your class. Writing `every { obj.someInline() }` records whatever the inlined body does, and MockK reports a missing-call error inside the block. `reified` functions are inline by definition, so the same applies. - **Top-level, `@JvmStatic` and Java `static` functions, and extension functions** — not instance calls on your mock; MockK reaches them with `mockkStatic`, a different, explicitly scoped operation. - **Constructors** — `SomeClass(...)` in production code does not consult your mock; that is `mockkConstructor` territory. - **JDK bootstrap classes** — instrumenting core platform classes is restricted and not something to rely on. - **Private functions** are not part of the ordinary DSL surface; MockK exposes them only through its dynamic-call syntax on a spy created with `recordPrivateCalls = true`. The practical takeaway: if a test needs any of these, that is usually a signal to inject the dependency instead.
code
kotlin · 11 linesclass Reporter {
fun name(): String = "reporter"
inline fun <reified T> render(value: T): String = "${T::class.simpleName}=$value"
}
val reporter = mockk<Reporter>()
every { reporter.name() } returns "stub" // fine: real call, real dispatch
// every { reporter.render(1) } returns "x" // body is inlined into this block;
// // no mock call is recorded -> MockK errorsgo deeper
Know that plain mockk() covers instance methods including final ones, and that statics, constructors and inline functions are different situations.
State the rule — interception needs a runtime call on a mock instance — and derive the inline / static / constructor cases from it.
Add the operational consequences: MockK's global patching entry points must be scoped and reverted, JDK bootstrap classes are off limits, and private members are only reachable via a spy's dynamic-call syntax.
Read the limits as architectural feedback: repeated need for static or constructor patching in code you own indicates missing injection seams, and set a team norm for when the global tools are acceptable.
## The rule behind all the cases MockK's agent rewrites a class's bytecode so each method body starts with a hook into MockK's dispatcher. Everything MockK can do with `mockk<T>()` follows from that one fact, and so does everything it cannot. Interception requires: 1. a **call that still exists at runtime**, and 2. a call that targets an **instrumented class**, and 3. a call **on a mock instance** MockK created. Break any of the three and a plain mock is powerless. ## Case 1 — inline functions (breaks rule 1) ```kotlin class Reporter { inline fun <reified T> render(value: T): String = "${T::class.simpleName}=$value" fun name(): String = "reporter" } ``` The Kotlin compiler copies `render`'s body into every caller. At runtime there is no `Reporter.render` invocation to hook — including inside your test's `every { }` block, where the body is inlined too. MockK sees no call on a mock and reports an error along the lines of "missing calls inside every { … } block". Candidates often misread this as a MockK bug; it is the compiler doing exactly what `inline` asks for. `reified` type parameters require `inline`, so every `reified` function is in this bucket. Fixes: drop `inline` if it was cargo-culted (most non-lambda-taking inline functions gain nothing), or have the inline function delegate to a normal member you can mock. ## Case 2 — statics, top-level and extension functions (breaks rule 3) `Instant.now()`, a top-level `fun parseConfig()`, a Java `static`, or an extension function are not dispatched on your mock instance — the caller does not go through any object you handed it. MockK does support them, but through a **different, deliberately scoped API** (`mockkStatic`), which patches globally and must be undone. That scoping difference is the point: mocking a static changes behaviour for everything in the JVM, whereas a mock instance affects only code you handed it to. ## Case 3 — constructors (breaks rule 3) If the code under test does `val client = HttpClient(cfg)`, no mock of yours is involved. MockK's `mockkConstructor` exists for this, again as a global, scoped patch. ## Case 4 — JDK bootstrap classes (breaks rule 2) Core platform classes loaded by the bootstrap classloader are restricted territory for instrumentation. Treat "mock `String`/core JDK internals" as not a thing to build a strategy on. ## Case 5 — private functions (not part of the surface) A private member is not something a caller can reference, so it is not part of a mock's DSL surface: you cannot write `every { mock.privateThing() }` because it does not compile. MockK's escape hatch is its dynamic-call syntax on a **spy** created with `recordPrivateCalls = true` — a niche tool for legacy code, not a normal technique. ## Reading the limits as design feedback Each limitation maps to a design property of the code under test: - needing to mock a **static or top-level function** means the dependency is reached through global state rather than injected; - needing to mock a **constructor** means the class builds its collaborators instead of receiving them; - needing to mock a **private function** means you are testing an implementation detail, or the class does too much; - needing to mock an **inline function** usually means `inline` was applied where it bought nothing. So the strongest senior answer is two-part: name the mechanism-level reason each case fails, then note that MockK's global patching tools exist for the cases you cannot refactor — third-party code, legacy call sites — and that reaching for them repeatedly in code you own is a signal about the seams, not about MockK. ## Interview framing The interviewer wants to know whether you understand *why*, not just *that*. Lead with "instrumentation intercepts calls, so anything with no runtime call on a mock instance is out", then enumerate: inline (compiled away), static/top-level/extension (not instance dispatch), constructors (not a call on your mock), JDK bootstrap (restricted), private (not a callable surface). Close with the injection point.
- Your test needs to control `Instant.now()` inside the class under test. What are the options and which do you prefer?A plain mock cannot help, because `Instant.now()` is a static call that never touches your mock. MockK can patch it with `mockkStatic`, which is global for the JVM until undone and therefore must be scoped and reverted. The preferable fix in code you own is to inject a `Clock` (or a time-provider lambda) and hand the test a fixed one — the dependency becomes explicit, the test needs no global patching, and the production code gains a seam it should have had.
- Someone says "mark it `open` and MockK will handle the inline function". Is that right?No — `open` is about virtual dispatch and overriding, which is not the problem here. An inline function's body is copied into the caller at compile time, so there is no call to dispatch at all, open or not. The real options are removing `inline` where it buys nothing, or having the inline function delegate to a normal member that the test can mock.
saying these in an interview costs you the question
- "MockK instruments bytecode so it can mock anything, including inline and static functions, with plain mockk()".
- "Making the class or function `open` fixes an inline function" — `open` is unrelated to inlining.
- "every { } on an inline function silently does nothing" — MockK reports a missing-call error inside the block; it is not silent.
- "mockkStatic and mockk() are the same thing with different names" — one is a global, scoped patch, the other affects only the instance you hand out.
- Presenting constructor and static mocking as the default technique rather than as a fallback for code you cannot change.