Skip to content

Dependency injection & testability — cheat sheet

From: fundamentals/17-dependency-injection-exercise.md. Concrete worked example of why interfaces and constructor injection matter, using a DamageCalculator that starts out hard to test because it creates its own Random internally.

The pattern (recognize this shape in an interview)

  1. Identify the untestable dependency — anything non-deterministic or external embedded directly inside a class: new Random(), a direct DB/HTTP call, System .currentTimeMillis(), etc. If it's new'd inside the class, the class owns it and a test can't control it.
  2. Extract it into its own class (SimpleRandomNumberGenerator wrapping Random), and inject that class via the constructor instead of instantiating it inside the class that needs it (DamageCalculator). Now at least the dependency is swappable at construction time.
  3. Extract an interface (RandomNumberGenerator) from that class, and have the consuming class (DamageCalculator) depend only on the interface — not on SimpleRandomNumberGenerator by name at all.
  4. Write a test double implementing the same interface (MockRandomNumberGenerator) that returns a controlled, fixed value instead of a real random one, and inject that in tests instead of the real implementation.

Result: the previously "random branch" of the code becomes fully deterministic and testable, without changing any of its actual logic — only how its dependency is obtained.

Why this matters (be ready to argue both sides)

Pros:

  • Full test coverage becomes possible, including branches that depend on non-deterministic behavior — by substituting a fake that behaves predictably.
  • The consuming class no longer knows or cares how the dependency does its job (same benefit as interfaces generally) — the real implementation could change entirely (a different RNG algorithm, a seeded one for reproducible test runs) without touching the consumer.

Cons:

  • More classes/indirection for what started as a couple of lines — real cognitive overhead.
  • Worth doing when a dependency is genuinely blocking testability or you need to swap implementations; not something to apply reflexively to every single dependency a class has.

This is precisely why Spring beans are typically declared as interface types and injected via the constructor: it's the same testability argument, just wired up by the framework instead of by hand (@MockBean in a Spring test plays the same role as MockRandomNumberGenerator here — see roadmap/03-testing.md).