Dependency Injection (course exercise walkthrough)
Java. A guided exercise showing why/how to extract a hard-to-test dependency (randomness) behind an injected interface, ending in a fully mockable, 100%-coverable class.
Starting point: untestable randomness
java
public class DamageCalculator {
private final Random random = new Random();
// Damage is attackPower * 100
// If the attack is a surprise attack, damage is multiplied by 2
// If the attack is a critical attack (calculated randomly by crit chance), damage is once again multiplied by 2
public int calculate(int attackPower, int critChanceInPercent, boolean isSurpriseAttack) {
boolean isCriticalAttack = random.nextInt(100) < critChanceInPercent;
int multiplier = 1;
if (isSurpriseAttack) {
multiplier *= 2;
}
if (isCriticalAttack) {
multiplier *= 2;
}
return attackPower * 100 * multiplier;
}
}Problem: calculate's outcome depends on Random, so a test can't assert an exact expected result for the critical-hit branch — the same input can produce two different outputs from run to run.
Exercise steps
- Write tests for the deterministic parts first. Refactor
calculateinto smaller methods that isolate the random decision (isCriticalAttack) from the pure arithmetic, so the arithmetic can be tested deterministically. Keepcalculate's signature unchanged. Run with coverage to see what's still untested (the random branch). - Extract the randomness into its own class:javaRemove
public class SimpleRandomNumberGenerator { private final Random random = new Random(); public int nextInt(int bound) { return random.nextInt(bound); } }RandomfromDamageCalculatorentirely (delete the import too); inject aSimpleRandomNumberGeneratorviaDamageCalculator's constructor instead of constructing it inside the class. - Extract an interface:java
public interface RandomNumberGenerator { int nextInt(int bound); }SimpleRandomNumberGenerator implements RandomNumberGenerator.DamageCalculatornow depends only on theRandomNumberGeneratorinterface — no mention ofSimpleRandomNumberGeneratorremains inside it. - Discuss pros/cons of this refactor (see below).
- Mocking: add a
MockRandomNumberGenerator implements RandomNumberGeneratorwith a package-visible field letting the test control exactly what "random" number it returns. Swap it in forSimpleRandomNumberGeneratorin the test. Now the critical-hit branch can be tested deterministically too — coverage ofDamageCalculatorreaches 100%.
Pros / cons of this refactor
Pros (what became possible that wasn't before):
DamageCalculatorbecomes fully testable, including the previously-untestable random branch, by substituting a controllable fake for the real randomness.DamageCalculatorno longer knows or cares how randomness is produced — it only knows theRandomNumberGeneratorcontract, so the real implementation could change (a different algorithm, a seeded generator for reproducible runs, etc.) without touchingDamageCalculatorat all.
Cons (the cost):
- More classes/files for what was originally a few lines — extra indirection and moving parts to hold in your head.
- Not free: this is a deliberate trade of some simplicity for testability/flexibility, worth it here because the randomness was genuinely blocking test coverage, but not something to reach for reflexively on every dependency.