skip to content

When would you deliberately not use MockK's @InjectMockKs and wire the subject under test by hand instead?

level: principalimportance: nice to knowfreq 24%

answer

  1. injection hides the dependency graph
  2. same-type deps ⇒ silent rewiring on rename
  3. hand wiring gives a compile error on change
  4. injectImmutable/overrideValues = escape hatches
  5. painful construction = design signal

basics

~20 s

When the wiring needs to be obvious and compile-checked: ambiguous same-type dependencies, subjects whose constructor changes often, or tests where readers must see exactly which collaborator is faked. Hand wiring fails at compile time; reflective injection fails silently or not at all.

solid answer

~60 s

`@InjectMockKs` trades explicitness for brevity, and the trade is only worth it in some places. Wire by hand when: - **Dependencies are ambiguous.** Two collaborators of the same type mean matching falls back to names; a rename rewires the test with no compile error. - **The constructor is churning.** Hand wiring turns a new dependency into a compile error that a human must resolve deliberately; reflective injection quietly supplies whatever it can and the test drifts from reality. - **The reader needs the seam.** A test whose point is "only the repository is faked, everything else is real" should show that in one visible constructor call. - **You would need `injectImmutable`/`overrideValues` to make it work.** Reflective writes into `val`s and replacement of already-set values are escape hatches; needing them is a signal, not a solution. Use it where it earns its keep: wide constructors of stable subjects, or legacy classes with property-set dependencies, where the boilerplate is real and the mapping is unambiguous. The deeper signal: if a subject is painful to construct by hand, the design — not the test — is usually what needs work.

code

kotlin · 13 lines
kotlin
// two dependencies of the SAME type: injection would match on names only
class Router(val primary: HttpClient, val fallback: HttpClient)

class RouterTest {
    @MockK lateinit var primary: HttpClient
    @MockK lateinit var fallback: HttpClient
    lateinit var router: Router

    @BeforeEach fun setUp() {
        MockKAnnotations.init(this)
        router = Router(primary = primary, fallback = fallback)  // compile-checked
    }
}

go deeper

for a junior

Know that @InjectMockKs is optional and that building the subject with a plain constructor call in setup is perfectly normal.

for a middle

Contrast compile-checked hand wiring with reflective matching, and name the same-type ambiguity as the concrete risk.

for a senior

Give a rule of thumb tied to failure modes — ambiguity, constructor churn, readability of the seam — and treat injectImmutable/overrideValues as escape hatches.

for a principal

Set a per-module policy and read the underlying signal: if hand wiring is unbearable, fix the subject's dependency surface instead of strengthening the injection.

## What the annotation actually buys you `@InjectMockKs` removes one line of setup: the constructor call that builds the subject from the declared doubles. In exchange, the wiring moves from source code the compiler checks into reflective matching performed at runtime by type and name. Every argument for or against it comes down to whether you want that trade in a given test class. ## The case for hand wiring **Compile-time feedback.** A hand-written `OrderService(repo, audit, clock)` breaks the build the moment the constructor changes. That is a feature: someone must look at the test and decide what the new dependency should be in this scenario. Reflective injection has no such moment — it supplies what it can match and stays silent about the rest, so a test can keep passing while testing a subject that no longer resembles production. **Ambiguity is resolved in the open.** When two dependencies share a type — a primary and a fallback client, two clocks, two caches — type matching cannot decide and names carry the meaning. Names are not part of any contract: rename a test property and the wiring silently changes. Hand wiring makes the assignment positional and explicit, and a mistake becomes a compile error or an obviously wrong argument. **Readability of the seam.** A well-written unit test communicates *which* collaborators are faked and which are real. One constructor call says that at a glance. With injection, the reader has to reconstruct the wiring from the annotations and MockK's matching rules. **Escape hatches signal trouble.** If the test only works with `injectImmutable = true` or `overrideValues = true` (or `@OverrideMockKs`), MockK is reaching into `val`s or replacing objects the subject built for itself. Both work, and both mean the test is fighting the subject's design. Sometimes that is the pragmatic choice for code you cannot change; it should never be the default posture. ## The case for using it **Wide, stable constructors.** A subject with eight collaborators that has not changed shape in a year produces a long, noisy constructor call in every test class. If types are distinct and the subject is stable, injection removes real boilerplate at little risk. **Property-injected legacy.** Classes whose dependencies are settable properties rather than constructor parameters are awkward to wire by hand — you construct, then assign, then hope you got them all. Injection handles it in one line, and here the reflective nature matches the design's own reflective flavour. **Consistency inside a suite.** A codebase that already uses annotations everywhere gains something from uniformity; mixing styles arbitrarily in one module costs more than either style alone. ## The judgment call to voice The interesting answer is not "annotations bad". It is that **injection hides a dependency graph, and hiding is only safe when the graph is unambiguous and stable.** Ask three questions: 1. Could two candidates match the same slot? If yes, wire by hand. 2. Does the constructor change often? If yes, you want the compile error. 3. Would a reader need to know exactly what is faked to understand the test? If yes, show the wiring. Otherwise, the annotation is fine. ## The design signal underneath There is a further point a principal-level answer should reach. If constructing the subject by hand is genuinely painful — a dozen dependencies, hidden collaborators created inside the class, `val`s that must be overwritten reflectively — the test is reporting a design problem. Reaching for stronger injection features makes the report go away without fixing the cause. The healthier move is usually to narrow the subject's dependencies or extract the collaborator that needs faking, so that a plain constructor call becomes adequate again. ## Practical policy A reasonable team rule: default to hand wiring for subjects with fewer than roughly four dependencies or with any repeated dependency type; allow `@InjectMockKs` for wide, stable constructors and for legacy property-injected classes; treat `injectImmutable` and `overrideValues` as requiring a comment that explains why the subject cannot be constructed normally. Whatever the rule, apply it per module rather than per test, so readers do not have to re-derive the convention in every file.

  • What concretely goes wrong when two dependencies share a type and you rely on @InjectMockKs?
    Type matching cannot pick between them, so the outcome hinges on name matching between your test properties and the subject's parameters or properties. Rename either side and the wiring changes with no compile error, so the subject may hold the double the test never stubs. The symptom is confusing: a verification fails claiming no interaction, while the code under test demonstrably called something.
  • Is there a middle ground between full annotation injection and fully manual construction?
    Yes — keep the collaborators as annotated properties, which is where the boilerplate really lives, and construct the subject by hand in the setup method after `MockKAnnotations.init(this)`. You keep the concise declarations and per-test freshness while the one line that matters for comprehension, the constructor call, stays explicit and compile-checked.

saying these in an interview costs you the question

  • Treating @InjectMockKs as always-better because it is shorter.
  • Ignoring that reflective wiring produces no compile error when the constructor changes.
  • Using injectImmutable or @OverrideMockKs routinely instead of treating them as escape hatches.
  • Claiming injection is unsafe in all cases, with no account of wide stable constructors or legacy property injection.
  • Missing the design signal: a subject that is painful to construct is telling you something about the production code.

context