skip to content

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?

level: principalimportance: should knowfreq 35%

answer

  1. Injectable → mockk<T>() (always first)
  2. object/companion → mockkObject
  3. top-level/ext/Java static → mockkStatic
  4. internally newed → mockkConstructor + anyConstructed
  5. Heavier tool = stronger refactor signal

basics

~20 s

Use 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 s

Pick 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

for a junior

Knows plain mockk is the default and that special tools exist for objects/statics.

for a middle

Maps each target type to the right tool and remembers teardown.

for a senior

Weighs blast radius/leakage and prefers DI, escalating deliberately.

for a principal

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

context