A test calls MockK's mockkStatic and stubs a Kotlin top-level function, but the real implementation still runs. What are the likely causes and how do you work through them?
answer
- inline = inlined at call site, unmockable
- facade renamed by @file:JvmName
- member or member extension, not top level
- widen to any() to split matching from instrumentation
- value computed before the patch was installed
basics
~20 sUsual causes: the function is inline so it was copied into the call site and there is nothing to intercept; the facade string is wrong because of a rename or @file:JvmName; the function is really a member or member extension, not a top-level one; the stub matched a different argument or receiver than the call used; or the call happened before the mock was installed.
solid answer
~50 sWork down a short list. 1. **`inline`** — an inline function is copied into the caller at compile time, so no call to the facade remains to intercept. Nothing MockK can do; change the seam. Most stdlib helpers are in this category. 2. **Wrong facade name** — the string must be the binary name of the file facade, and `@file:JvmName` or a file rename changes it. Verify against the compiled class name. 3. **Wrong kind of declaration** — a member function, or an extension declared inside a class/object, is not on the facade at all; you must mock the declaring type instead. 4. **Stub never matched** — a literal argument or literal extension receiver in `every` only matches equal values. Widen to `any()` temporarily; if the stub then hits, it was a matching problem, not an instrumentation problem. 5. **Ordering** — the value was produced before `mockkStatic` ran (an initializer, a cached `val`, a field set in `@BeforeAll`), or another test's `unmockkAll` removed the registration first.
code
kotlin · 6 linesmockkStatic("com.acme.GreetingsKt")
every { greet(any()) } returns "stubbed" // widen everything
service.welcome()
verify { greet(any()) } // records nothing -> not instrumented at allgo deeper
List the two most common causes — inline functions and a wrong facade string — and know that unstubbed or unmatched calls run for real.
Give an ordered diagnosis: widen matchers, check the declaration, verify the facade name, then check ordering and cleanup.
Add the lifecycle causes — values computed before setup, shared unmockkAll teardown, tests sharing a JVM — and say when you would abandon the seam entirely.
Point out that a symptom this silent is an argument against static mocking for widely-used helpers, and set a team rule for where it is allowed.
## Why the symptom is confusing Static mocking is spy-like: unmatched calls fall through to the real implementation silently. So a test where instrumentation never happened and a test where the stub simply did not match look identical from the outside — real behaviour, no error. Diagnosis is therefore about separating *not instrumented* from *not matched*. ## Step 1: is the function inlinable? Check the declaration. `inline fun` means the compiler pastes the body into every call site; the call to the facade method does not exist in the caller's bytecode, so there is nothing to intercept. This covers a large part of the Kotlin standard library and any project helper written for zero-overhead lambdas. `reified` implies `inline`, so those are unmockable too. This is a hard limitation, not a configuration issue: if you must substitute the behaviour, route it through a non-inline function or an injected collaborator. ## Step 2: is the name right? The facade name is `<package>.<FileName>Kt`, changed by `@file:JvmName`, and unaffected by directory structure. A file rename during a refactor breaks the string with no compile error. Confirm the name from the compiled output rather than reasoning about it. Note the two failure shapes: a name that resolves to a *different existing class* patches something irrelevant and looks exactly like this bug, while a name that resolves to nothing fails at the `mockkStatic` call itself. ## Step 3: is it really top level? Only top-level declarations live on the facade. Three lookalikes do not: - a member function of a class -> mock or spy the instance; - an extension declared inside a class or object -> mock the declaring type; - a member of an `object` or `companion object`, including `@JvmStatic` ones -> that is object mocking, a different tool. Read the declaration site, not the call site: `Formatting.shout(x)` and `x.shout()` can both be members. ## Step 4: did the stub match? Even a correctly patched class only substitutes calls that match a recording. Frequent mismatches: - a literal argument (`every { greet("bob") }`) while the code calls `greet("Bob")`; - a literal **receiver** on an extension — the receiver is argument zero and matches by the same rules; - an overload: you stubbed `format(Int)` and the code calls `format(Long)`; - default parameters: the call site passes different effective arguments than you assumed. The cheap experiment is to widen everything to `any()`. If the stub now takes effect, instrumentation was fine and you have a matching bug; if it still does not, you are in steps 1-3. A `verify` on the same widened signature is the second probe: if verification also records nothing, the call is not reaching the patched class at all. ## Step 5: ordering and lifecycle Instrumentation only affects calls made **after** `mockkStatic` runs. - A value computed in a field initializer, an `init` block, a `companion object` initializer or a `@BeforeAll`-style hook may already have been produced. - A cached result — a `val` holding the outcome of the function, a lazily initialised singleton — is not recomputed just because you later patched the class. - Cleanup from elsewhere can remove your registration: a shared teardown calling `unmockkAll()` that runs between your setup and your assertion, or another test doing the same in a suite that executes tests concurrently in one JVM. ## Step 6: the call is not where you think Finally, confirm the production path actually calls that function. Kotlin makes it easy to have two same-named helpers in different files, or for the code under test to reach the behaviour through a wrapper you did not patch. Put a temporary `verify { ... }` or a breakpoint in the real implementation and see which one runs. ## The order that pays off In practice: widen matchers first (one edit, rules out half the causes), then check the declaration for `inline` and for where it is declared, then confirm the facade name against the build output, then look at ordering and shared teardown. That sequence resolves nearly every instance of this symptom.
- Why can MockK never intercept an inline function, and what do you do instead?An inline function's body is copied into each call site at compile time, so the caller's bytecode contains no call to the facade method that MockK patched. There is nothing left to intercept. The remedies are to make the function non-inline if you own it, to wrap the behaviour behind an injectable interface, or to test the real behaviour rather than substituting it.
- You widened all matchers to any() and the stub now works. What did that tell you?That instrumentation was correct all along — the class was patched and the call did reach MockK — and the failure was a matching problem. Next you narrow back one argument at a time, or capture the arguments, to find which value differed: a case-sensitive string, an extension receiver, a different overload, or a default parameter you assumed was something else.
saying these in an interview costs you the question
- Assuming mockkStatic silently failing means MockK is broken rather than the function being inline or the name being wrong
- Not knowing that unmatched calls fall through instead of erroring
- Confusing a member extension with a top-level extension
- Ignoring that a cached value or an initializer may have run before the patch
- Blaming instrumentation without first widening matchers to any()