skip to content

Compare MockK's @MockK, @RelaxedMockK and @SpyK: what object does each property end up holding, and how must each property be declared?

level: middleimportance: should knowfreq 40%

answer

  1. annotation ≙ factory call
  2. @MockK fills a lateinit slot
  3. @SpyK wraps a value you supply
  4. @RelaxedMockK == @MockK(relaxed = true)
  5. spy initialiser runs before init(this)

basics

~20 s

@MockK and @RelaxedMockK both put a mock in the property and go on lateinit var of the mocked type; @RelaxedMockK equals @MockK(relaxed = true). @SpyK wraps a real value you supply, so its property must be an initialised var, not lateinit.

solid answer

~50 s

All three are declarations that `MockKAnnotations.init(this)` turns into objects. - **`@MockK lateinit var repo: Repo`** — MockK constructs the double; the property holds a mock of the declared type. Because MockK supplies the value, `lateinit var` is the natural form. The annotation carries the same creation options as the `mockk()` function, so `@MockK(relaxed = true)` and `@MockK(relaxUnitFun = true)` are available. - **`@RelaxedMockK lateinit var repo: Repo`** — the same thing, just a shorter spelling of `@MockK(relaxed = true)`; nothing else differs. - **`@SpyK var clock = FixedClock(EPOCH)`** — a spy wraps an existing object, so *you* provide the instance in the property initialiser and MockK replaces it with a spy around it. `lateinit` does not work here: there would be nothing to wrap. The declaration shape follows directly from who supplies the object: MockK for mocks, you for spies. Everything else — relaxation choices, stubbing, verification — is identical to the equivalent `mockk()`/`spyk()` call.

code

kotlin · 9 lines
kotlin
class CheckoutTest {

    @MockK lateinit var repo: OrderRepo          // = mockk<OrderRepo>()
    @MockK(relaxUnitFun = true) lateinit var bus: EventBus
    @RelaxedMockK lateinit var audit: Audit      // = mockk<Audit>(relaxed = true)
    @SpyK var clock = FixedClock(EPOCH)          // = spyk(FixedClock(EPOCH))

    @BeforeEach fun setUp() = MockKAnnotations.init(this)
}

go deeper

for a junior

Know the three annotations and the declaration each needs: lateinit var for mocks, an initialised var for a spy.

for a middle

Map each annotation to its factory call and its parameters, and explain why the declaration shape differs — who supplies the object.

for a senior

Point out the consequences: spies run real code, property initialisers execute before init, and all kinds are injectable into the subject.

for a principal

Standardise one spelling across the codebase and be explicit about when a spy is acceptable at all, since spies bring real behaviour and real side effects into a unit test.

## One idea, three declarations MockK's annotation set exists so a test class can *declare* its collaborators as properties instead of building them in a setup body. Each annotation corresponds to a factory function you could have called by hand, and the annotation's parameters are the same parameters that function takes. Understanding the mapping removes all the mystery: | Annotation | Equivalent call | Who supplies the underlying object | |---|---|---| | `@MockK` | `mockk<T>()` | MockK | | `@MockK(relaxed = true)` / `@RelaxedMockK` | `mockk<T>(relaxed = true)` | MockK | | `@MockK(relaxUnitFun = true)` | `mockk<T>(relaxUnitFun = true)` | MockK | | `@SpyK` | `spyk(existingObject)` | **you** | ## Why the declaration form differs For `@MockK`, the property is a slot MockK fills. The type on the property is what MockK mocks, and there is no value for you to write — hence `lateinit var`. Using `val` would leave nothing to assign; using `var x: Repo? = null` works mechanically but forces null handling all through the test, so the idiom is `lateinit var`. For `@SpyK`, the semantics are different in kind. A spy is a wrapper around a real instance: calls go through to real behaviour unless a stub says otherwise. That real instance has to come from somewhere, and MockK cannot invent it — it does not know which constructor arguments your object needs. So the property is written with an initialiser, e.g. `@SpyK var clock = FixedClock(EPOCH)`, and `init` swaps in a spy that wraps that value. This is also why a `lateinit` `@SpyK` is a mistake: at initialisation time there is no value to wrap. ## @RelaxedMockK is pure sugar `@RelaxedMockK` produces exactly what `@MockK(relaxed = true)` produces. It exists because relaxed mocks are common enough in Kotlin tests that a dedicated annotation reads better in a long property list. Nothing about lifecycle, injection eligibility or verification changes. Teams normally pick one spelling and use it consistently; mixing both in one class makes readers hunt for a difference that is not there. ## Consequences for injection All three kinds are candidates for `@InjectMockKs`: when MockK wires the subject under test, it draws from the doubles it has created in that class, whether they are mocks or spies. So a subject can legitimately receive a mocked repository and a spied clock in the same wiring pass. Since spies carry real behaviour, this is a common way to keep one collaborator honest while faking the rest. ## Ordering consequences The `@SpyK` initialiser is an ordinary property initialiser and therefore runs when the test instance is constructed — *before* `MockKAnnotations.init(this)`. That is fine as long as the initialiser does not depend on another annotated property: `@SpyK var clock = FixedClock(EPOCH)` is safe, whereas an initialiser that reads a `@MockK lateinit` property would throw because that property is not assigned yet. The rule is simple: property initialisers may only use things that exist at construction time. ## What does not change Everything downstream is identical to the function-call style. `every`/`coEvery` stub the annotated mock exactly as they stub a hand-built one; `verify`/`coVerify` work the same; clearing and lifecycle rules are the same. Choosing annotations over factory calls is a readability decision about where collaborators are declared, not a behavioural one. ## Common mistakes - **`@SpyK lateinit var`** — no instance to wrap. - **Expecting `@MockK` on a `val` with an initialiser to be replaced** — a plain `@MockK` on an already-assigned property is not how mocks are declared; mocks belong on `lateinit var` slots MockK fills. - **Believing `@RelaxedMockK` differs from `@MockK(relaxed = true)`** — it does not. - **Assuming a spy created by `@SpyK` is a mock** — it holds real behaviour and will execute real code for anything you do not stub, which can mean real side effects. ## How to answer Structure it as *who supplies the object*: MockK fills `@MockK`/`@RelaxedMockK` slots, so they are `lateinit var`; you supply the instance for `@SpyK`, so it is an initialised `var`. Then add that `@RelaxedMockK` is sugar for `@MockK(relaxed = true)`, that the annotation parameters mirror the `mockk()` parameters, and that all of them are eligible for injection into the subject.

  • Why can't @SpyK be used on a lateinit property?
    A spy is a wrapper around an existing object: MockK needs a real instance to delegate to. A `lateinit` property has no value at initialisation time, and MockK cannot guess how to construct one, so there is nothing to wrap. Declaring it as an initialised `var` supplies the instance; MockK then replaces the property's value with a spy around it.
  • Can a @SpyK property participate in @InjectMockKs wiring of the subject?
    Yes. Injection draws from all the doubles the same initialisation created, spies included, so a subject can receive mocked collaborators and a spied one together. That is a common pattern for keeping one dependency's real behaviour — a clock, a formatter, a pure calculator — while faking the ones that talk to the outside world.

saying these in an interview costs you the question

  • Claiming @RelaxedMockK behaves differently from @MockK(relaxed = true).
  • Declaring @SpyK on a lateinit property with no instance to wrap.
  • Thinking a spy is just a mock with defaults, and forgetting it executes real code for unstubbed calls.
  • Believing annotated mocks behave differently from ones built with mockk() in a setup method.

context