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)
- 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'snew'd inside the class, the class owns it and a test can't control it. - Extract it into its own class (
SimpleRandomNumberGeneratorwrappingRandom), 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. - Extract an interface (
RandomNumberGenerator) from that class, and have the consuming class (DamageCalculator) depend only on the interface — not onSimpleRandomNumberGeneratorby name at all. - 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).