Given a dependency you need to fake, how do you choose between mockkObject, mockkStatic, mockkConstructor, and a plain mockk — and when would you refactor instead?
answer
- Injectable → mockk<T>() (always first)
- object/companion → mockkObject
- top-level/ext/Java static → mockkStatic
- internally newed → mockkConstructor + anyConstructed
- Heavier tool = stronger refactor signal
basics
~20 sUse a plain mockk for normal injectable classes. Use mockkObject for Kotlin objects/companions, mockkStatic for top-level/extension/Java-static functions, and mockkConstructor for objects newed up internally. The heavier the tool, the more it hints you should refactor to inject the dependency instead.
solid answer
~50 sPick by **what the call target is**: a regular injectable collaborator → `mockk<T>()` (cleanest, scoped, no global state). A Kotlin `object`/`companion object` singleton → `mockkObject`. A top-level function, extension function, or Java `static` → `mockkStatic`. A collaborator the code `new`s up internally with no seam → `mockkConstructor` + `anyConstructed<T>()`. The three static-family tools mutate global JVM state and need explicit teardown, so they're progressively heavier and more refactor-smelly. The senior judgement: each time you reach past `mockk<T>()`, ask whether the design is hiding a dependency. Usually you can refactor — inject an interface (a `Clock`, `IdGenerator`, repository), pass a factory `() -> Foo`, or wrap a third-party static behind your own thin interface — so production code is testable with a plain mock and no bytecode patching. Reserve static-family mocking for third-party/framework code you cannot change (Android `Log`, `System.currentTimeMillis`, library statics).
go deeper
Knows plain mockk is the default and that special tools exist for objects/statics.
Maps each target type to the right tool and remembers teardown.
Weighs blast radius/leakage and prefers DI, escalating deliberately.
Treats heavy mocking as architectural feedback, sets team policy (inject Clock/IdGenerator, wrap third-party statics) and confines static-family mocking to unchangeable code.
## A decision tree ``` Is the dependency already injectable (passed in / a field)? └─ yes → mockk<T>() (scoped, no global state — preferred) └─ no, it's a Kotlin object / companion → mockkObject(Obj) └─ no, it's a top-level / extension / Java static fn → mockkStatic(::fn | Class::class | "PkgKt") └─ no, it's newed inside the code under test → mockkConstructor(Foo::class) + anyConstructed<Foo>() ``` ## Why ordering matters: cost and blast radius - **`mockk<T>()`** — local, dies with the variable, zero leakage. Cheapest, always first choice. - **`mockkObject`** — patches one shared singleton; spy-like; needs teardown. - **`mockkStatic`** — patches a global static call site; affects the whole process; needs teardown and can hit dispatch subtleties (extension receiver types, `…Kt` names). - **`mockkConstructor`** — installs a constructor interceptor for all future instances; most invasive, most fragile (inline/value-class caveats); needs teardown. The further down you go, the more global state you touch and the more the test couples to implementation details rather than behaviour. ## The refactor lens (the principal-level point) Reaching for the heavy tools is usually a **design signal**, not just a testing choice: - Internal `Clock()`/`System.currentTimeMillis()` → inject a `Clock` interface; mock it plainly. - `companion object` factory → inject a factory or a provider so you don't `mockkObject`. - Internally newed collaborator → constructor-inject it or a `() -> Collaborator` factory. - Third-party static you can't change → wrap it in a thin interface you own (`interface Logger`), implement once, mock the interface. ```kotlin // Smell: hard to test without mockkStatic fun expired(t: Long) = System.currentTimeMillis() - t > TTL // Refactored: trivially testable with a plain mockk / fake fun interface Clock { fun now(): Long } class Expiry(private val clock: Clock) { fun expired(t: Long) = clock.now() - t > TTL } ``` ## When static-family is the right call It's legitimate for code you genuinely cannot restructure: Android framework statics (`Log`, `TextUtils`), JDK statics, or third-party top-level helpers. There, `mockkStatic`/`mockkObject` is pragmatic — just isolate those tests, tear down globally, and don't let the pattern spread into your own injectable code. ## Summary heuristic Prefer **plain `mockk` + dependency injection**; escalate to object → static → constructor only as the target's nature forces you, and treat each escalation as a prompt to consider a small refactor.
- You must fake System.currentTimeMillis() in your own service. mockkStatic or refactor?Refactor: introduce a Clock interface and inject it, then use a plain mockk or a fake. Reserve mockkStatic for statics you cannot change.
- Why is mockkConstructor considered the heaviest of the three?It intercepts all future construction of a type via a global bytecode interceptor, has inline/value-class limitations, couples the test to internal instantiation, and most often signals a missing injection seam.
Plain mockk is using the door; the static-family tools are climbing through the window — fine for a locked third-party house, but if it's your own house, install a door (inject the dependency).
saying these in an interview costs you the question
- Reaching for mockkStatic/mockkConstructor on code you control instead of injecting
- Not recognising the static-family tools as global-state / design smells
- Choosing the tool by habit rather than by what the call target actually is
- Claiming plain mockk can fake a Kotlin object or a top-level function