skip to content

Object and Companion Mocking

mockkObject swaps behavior on a singleton object or companion in place. Interviewers ask it because Kotlin singletons are everywhere and Java mocking has no answer for them.

on this pageshow

explore

questions

4

When you call MockK's mockkObject(PricingRules) on a Kotlin object declaration, is a new instance created to stand in for the singleton, and what happens when the test then calls a method you never stubbed?

level: middleimportance: must knowfreq 40%

answer

  1. patched in place — same instance, same state
  2. object mock = spy: unstubbed calls run real code
  3. no relaxed flag on mockkObject
  4. stub every reachable overload
  5. unmockk restores behaviour, not mutated state

basics

~20 s

No new instance: MockK patches the existing singleton in place, so its identity and state are unchanged and every holder of a reference sees the patch. Unstubbed methods still run the real implementation — an object mock is a spy, not a strict mock.

solid answer

~50 s

`mockkObject` does not create a replacement object. MockK instruments the singleton that already exists so its method calls route through MockK's handler; the reference is the same instance, `PricingRules === PricingRules` still holds, its properties keep their values, and any code that captured the reference earlier is affected too. The behaviour that surprises people is that an object mock is **spy-like**: there is no relaxed flag and no strict mode. A call you have stubbed with `every` returns your answer; a call you have not stubbed executes the real body. Compare that with `mockk<T>()`, where an unstubbed call fails loudly. So the classic bug is silent rather than noisy — you stub one overload, the code calls another, and the real implementation runs (hitting real I/O or real clocks) while the test looks fine. `unmockkObject` removes the patch and restores plain real behaviour.

code

kotlin · 12 lines
kotlin
object PricingRules {
    fun surcharge(amount: Int) = amount / 10
}

mockkObject(PricingRules)
assertEquals(10, PricingRules.surcharge(100))   // real implementation

every { PricingRules.surcharge(any()) } returns 0
assertEquals(0, PricingRules.surcharge(100))    // stubbed

verify { PricingRules.surcharge(100) }          // recorded either way
unmockkObject(PricingRules)

go deeper

for a junior

Know that the real singleton is patched rather than replaced, and that anything you did not stub still runs for real.

for a middle

Explain the in-place model — identity, state and existing reference holders — and why the spy default turns a missed stub into a silent bug rather than a loud one.

for a senior

Use it deliberately: patch for observation, stub every reachable overload, and treat unexplained real I/O in a test as evidence of an unstubbed member.

for a principal

Frame the spy default as the cost of having no injection seam at all, and weigh that against giving the singleton an interface for the code paths that matter.

## What a Kotlin object is, and why mocking it is special A Kotlin `object` declaration is a singleton the language instantiates for you; there is exactly one instance and no constructor to intercept. Call sites do not receive it as a parameter — they reference it directly, so there is no seam to substitute at. That is precisely the gap `mockkObject` fills, and understanding *how* it fills it explains almost every question people have about it. ## In-place patching, not substitution MockK does not build a fake and swap it in — there is nowhere to swap it in *to*. Instead it uses its JVM instrumentation to make calls on that existing instance dispatch through MockK's handler. Three consequences follow, and they are the whole mental model: 1. **Identity is preserved.** The singleton is still the same object. Code that stored the reference in a field, captured it in a lambda, or registered it in a listener list minutes earlier is affected the moment you call `mockkObject`, with no re-wiring. 2. **State is preserved.** MockK does not reset properties. If some earlier code mutated a counter on the object, it keeps its value; if a stubbed call would have mutated it, it no longer does, because the real body never ran. 3. **The patch is not scoped to your test.** It applies to the loaded class in the JVM, so it is visible to every thread and every other test running in that process until it is undone. ## Object mocks are spies This is the single most-asked mechanic. `mockkObject` has no `relaxed` parameter and no strict mode: after patching, an unstubbed method **calls through to the real implementation**. ``` object PricingRules { fun surcharge(amount: Int) = amount / 10 } mockkObject(PricingRules) PricingRules.surcharge(100) // 10 — real code still runs every { PricingRules.surcharge(any()) } returns 0 PricingRules.surcharge(100) // 0 unmockkObject(PricingRules) ``` Contrast this with `mockk<PricingRules>()`-style mocking of an interface or class, where an unstubbed call throws "no answer found". The difference matters because it changes the shape of your bugs. With a strict mock, a missed stub is a loud failure that points at the exact call. With an object mock, a missed stub is *silence*: the real method runs, possibly opening a connection, reading a clock, or writing a file, and the test may still pass — until it fails on a machine where the real behaviour differs. The practical rule: after `mockkObject`, stub **every** member the code under test can reach, including overloads. Stubbing `load(id: String)` does nothing for a call to `load(id: String, refresh: Boolean)`. ## Why calls can still be verified Because dispatch goes through MockK's handler even when it calls through, calls are recorded. `verify { PricingRules.surcharge(100) }` works whether or not the method was stubbed — the recording is a property of the patch, not of stubbing. That makes `mockkObject` genuinely useful as a pure observer: patch, run, verify, restore, with real behaviour throughout. ## Undoing it `unmockkObject(PricingRules)` removes the patch and returns the singleton to plain real behaviour. The block-scoped overload — `mockkObject(PricingRules) { ... }` — does the same at block exit and keeps the patched window narrow, which is the habit worth forming. What restoring does *not* do is roll back the world: real property mutations that happened while the object was patched, and side effects that real unstubbed code performed, remain exactly as they are. Restoration is about behaviour, not about state. ## The mental checklist When someone says "my object mock isn't working", walk this list: is the code actually calling the member you stubbed, or a different overload? Is it calling a top-level function rather than an object member (that is a different tool)? Is it constructing an instance rather than going through the singleton (again a different tool)? And did an earlier test leave the object patched or leave its state mutated? Almost every real report resolves to "the real implementation quietly ran", which is exactly what the spy model predicts.

  • Can you verify calls on a mocked object member that you never stubbed?
    Yes. Once the object is patched, calls dispatch through MockK's handler and are recorded even when they call through to the real body. That makes mockkObject usable as a pure observer: patch, exercise real behaviour, verify the interaction, then unmock. Recording is a property of the patch, not of stubbing.
  • Why is a missed stub on an object mock more dangerous than a missed stub on a plain mockk<T>()?
    A plain mock throws on an unstubbed call, so the mistake is loud and points at the exact member. An object mock silently runs the real implementation, which may touch a database, the network or the system clock while the test still passes locally. The failure then shows up later, on a different machine or in a different order, far from its cause.

It is not a body double walking onto set; it is a wiretap on the original. Lines you scripted get replaced, everything else the original says goes out unchanged — and everyone who already knew the person is now talking to the tapped line.

saying these in an interview costs you the question

  • "mockkObject replaces the singleton with a fresh empty mock"
  • Expecting an unstubbed member to throw or return null/0
  • Looking for a relaxed = true parameter on mockkObject
  • Assuming references captured before the call are unaffected
  • Believing unmockkObject also resets the object's properties

context

open as a page

MockK's mockkObject patches a singleton in place rather than substituting it. Concretely, who can observe that patch while it is active, and what does unmockkObject give back — and what does it not give back?

level: seniorimportance: must knowfreq 34%

basics

~20 s

Everything in that JVM sees it: all threads, all concurrently running tests, all reference holders, and any production path reaching the singleton. There is no per-test or per-thread scoping. unmockkObject restores real behaviour only — not the object's mutated state, nor side effects real code already performed.

open as a page

A Kotlin class exposes a factory in its companion object, called as Report.of(row). How do you target that companion with MockK so the factory returns a canned Report, and what are the usual reasons such a stub appears to have no effect?

level: middleimportance: should knowfreq 33%

basics

~20 s

Patch the companion instance: mockkObject(Report.Companion) — writing mockkObject(Report) works too, since a bare class name with a companion resolves to that companion instance — then every { Report.of(any()) } returns fixture. Stubs miss when a different overload, a constructor call, or a top-level function is what production actually calls.

open as a page

Your team enabled parallel test execution inside a single JVM, and the tests using MockK's mockkObject started failing intermittently in ways nobody can reproduce locally. How would you reason about the cause, and what policy would you set for object mocking across the suite?

level: principalimportance: nice to knowfreq 19%

basics

~20 s

Object patching is process-global, so parallel tests share it: one stubs while another expects real behaviour, or one unmocks mid-flight. Policy: keep patched windows minimal and scoped, never unmockkAll in shared teardown under parallelism, isolate object-mocking tests from concurrent peers, and remove the need where a seam is affordable.

open as a page