skip to content

MockK lets you write either spyk(MyService(...)) or spyk<MyService>(). How does the underlying instance come into existence in each case, and when does the second form blow up at runtime?

level: middleimportance: should knowfreq 32%

answer

  1. spyk(instance) = constructed + fields copied
  2. spyk<T>() = no constructor runs
  3. fields default to null/0/false
  4. lateinit not initialized → wrong spyk form
  5. stateless class only for spyk<T>()

basics

~10 s

spyk(MyService(...)) spies a normally constructed object and copies its state. spyk<MyService>() creates the instance without running any constructor, so fields hold defaults — null, 0, false — and real methods that touch them fail.

solid answer

~50 s

Both forms produce an instrumented instance of the class whose unstubbed methods run real code; they differ in how that instance gets its **state**. - `spyk(MyService(dep))` — you construct the object yourself, normally, and MockK copies its field values into the spy. Property initialisers and `init` blocks have run, so the spy behaves like a real, initialised object. - `spyk<MyService>()` — MockK creates the instance itself **without invoking a constructor**. No property initialiser or `init` block runs. Every field holds its JVM default: `null` for references, `0` for numbers, `false` for booleans. So the second form is fine for a stateless class, or when you intend to stub every method you exercise. It blows up the moment a real method dereferences an uninitialised field — an NPE, or `lateinit property ... has not been initialized`. It is genuinely useful when the constructor is expensive or does I/O you do not want in a unit test, but then you must stub whatever touches state.

code

kotlin · 10 lines
kotlin
class OrderService(private val repo: OrderRepo) {
    lateinit var region: String
    fun label(id: Long) = "${region}-${repo.find(id).code}"
}

val s1 = spyk(OrderService(repo).apply { region = "EU" })
s1.label(1L)   // real code, real fields — works

val s2 = spyk<OrderService>()
s2.label(1L)   // UninitializedPropertyAccessException: lateinit property region ...

go deeper

for a junior

Remember the practical rule: pass a real constructed object to spyk unless you have a reason not to.

for a middle

Explain that spyk<T>() creates the object without running any constructor, so fields hold JVM defaults, and name the two failure modes (NPE, lateinit not initialized).

for a senior

Show the judgement: use the constructor-free form only for stateless classes or when every reached method is stubbed, and read constructor pain as a design signal.

for a principal

Treat repeated need for constructor-free spies as evidence that construction and I/O are tangled, and drive the refactor rather than institutionalising the workaround.

## Two ways to get a spy MockK's `spyk` has two shapes: ```kotlin val a = spyk(PriceCalculator(taxTable)) // spy a real, constructed instance val b = spyk<PriceCalculator>() // spy a class, MockK makes the instance ``` Both give you the same *behavioural* contract — unstubbed members execute the class's real code, every call is recorded for `verify`, and you can override selected members with `every`. What differs is where the object's fields come from. ## Form one: spyk(instance) You build the object with a normal constructor call. All the usual Kotlin initialisation has happened: constructor parameters assigned, property initialisers evaluated, `init` blocks executed, `lateinit` properties possibly set. MockK then copies those field values into its own instrumented instance. The spy starts life fully initialised, and real methods behave as they do in production. Cost: you must be able to construct the class in a test. If the constructor demands a database handle, opens a socket, or reads configuration, that cost lands in your test. ## Form two: spyk<T>() Here MockK creates the instance itself, and — this is the part candidates miss — **it does not call a constructor**. Object creation for mocks bypasses constructors entirely, which is what lets MockK mock classes whose constructors are unpleasant. The resulting object is structurally a `T`, but every field sits at its JVM default: - reference fields: `null` - numeric fields: `0` / `0.0` - boolean fields: `false` Nothing in your class ever assigned them, because nothing in your class ran. ### What that means for real method calls A spy runs real code for anything unstubbed. So the first unstubbed method that reads a field explodes in one of two ways: - a plain `NullPointerException` deep inside your own code, because a non-null-typed field is actually `null`; - `UninitializedPropertyAccessException: lateinit property repository has not been initialized`, because `lateinit` checks the backing field for null on read and MockK left it null. The stack trace points at *your* class, not at MockK, which is why this is confusing the first time. ### When form two is the right choice - The class is effectively stateless — pure functions over parameters. Then the empty fields never matter. - The constructor is expensive or does I/O, and you intend to stub every method the test will actually reach, so no real code touches a field. - You are testing one algorithmic method that only uses its arguments. In every other case, prefer `spyk(RealThing(...))`. If constructing the real thing is painful, that pain is information about the class's design, and hiding it behind a constructor-free spy usually trades a clear failure for a mysterious one. ## Choosing deliberately A good rule: pick `spyk(instance)` by default, and reach for `spyk<T>()` only when you can articulate why no real code will read state. If you find yourself stubbing more and more methods on a `spyk<T>()` to keep it from throwing, you have effectively built a mock the hard way — create a mock instead. ## Related knobs at creation time `spyk` also accepts a `name` (which shows up in MockK's error and verification messages, making failures readable when several spies are in play) and `recordPrivateCalls` (see private-call recording). Both apply to either form. ## Diagnosing it in the wild Symptom: a spy-based test throws an NPE or `UninitializedPropertyAccessException` from inside production code that plainly cannot be null in production. Check the spy's construction form. If it is `spyk<T>()`, either switch to spying a constructed instance, or stub the method that is reading the field so the real code never runs.

  • If the constructor of the class does real I/O, how would you still get a spy you can safely call?
    Use `spyk<T>()` so the constructor never runs, then stub every method the test will actually invoke so no real code reads the empty fields — at which point a plain mock is usually the simpler tool. The better long-term answer is to move the I/O out of the constructor into an injected collaborator, which makes the class constructible in tests.
  • Does spyk<T>() also skip init blocks and property initialisers, or only the constructor body?
    It skips all of them. In Kotlin, property initialisers and `init` blocks are compiled into the constructor, so bypassing constructor invocation bypasses the whole initialisation sequence. Nothing the class declares as a default value is ever assigned.

saying these in an interview costs you the question

  • Believing spyk<T>() calls the no-arg constructor
  • Blaming MockK for an NPE that is really an uninitialised field on a constructor-free spy
  • Assuming spyk<T>() and spyk(T()) are interchangeable
  • Stubbing method after method on a spyk<T>() instead of switching to a mock
  • Thinking a class must have a no-arg constructor to be spied at all

context