Chapter 12 of 18
12 — Mocking
Hand-rolled fakes and spies — test the choreography, not the wait.
Kata: a Countdown that prints 3, 2, 1, Go! with a one-second pause between numbers —
tested without waiting three seconds.
New ideas: hand-rolled test doubles: a fake (RecordingWriter) and a spy (RecordingSleeper).
This chapter is a kata — write the code yourself. If you’re following along inside the solutions repo, the finished class also ships in
src/main/java/mocking— don’t open it yet. Write the code from the steps, run the tests, and let the failures drive you.
Why we need doubles
A real countdown does Thread.sleep between numbers. A test that let it sleep would take seconds
per run — and worse, would depend on a wall clock. The insight: the behaviour to verify is
“write line, then sleep, then write next line” — not “actually waited”. So you inject the seams
(chapter 11) and provide doubles that record instead of sleeping.
Step 1 — RED: write the ordering test first
This is the single test the chapter lives on. It asserts what happened in the order it happened:
@Test
@DisplayName("writes each number, sleeps one second, then Go")
void countsDownAndStarts() {
RecordingWriter writer = new RecordingWriter();
RecordingSleeper sleeper = new RecordingSleeper();
Countdown countdown = new Countdown(writer, sleeper);
countdown.run();
assertEquals(List.of("3\n", "2\n", "1\n", "Go!\n"), writer.lines());
assertEquals(
List.of(Duration.ofSeconds(1), Duration.ofSeconds(1), Duration.ofSeconds(1)),
sleeper.sleeps());
}
Run gradle test --tests "mocking.CountdownTest":
error: cannot find symbol
Countdown countdown = new Countdown(writer, sleeper);
^
symbol: class Countdown
RED. The test simultaneously declares three types: Countdown, and two seams it can be
constructed with.
Step 2 — declare the seams so the test can compile
@FunctionalInterface
public interface Writer {
void write(String line);
}
@FunctionalInterface
public interface Sleeper {
void sleep(Duration duration);
}
Now the test needs the doubles it already refers to (RecordingWriter, RecordingSleeper). Small
and bespoke — that’s the point:
private static final class RecordingWriter implements Writer {
private final List<String> lines = new ArrayList<>();
@Override public void write(String line) { lines.add(line); }
List<String> lines() { return List.copyOf(lines); }
}
private static final class RecordingSleeper implements Sleeper {
private final List<Duration> sleeps = new ArrayList<>();
@Override public void sleep(Duration duration) { sleeps.add(duration); }
List<Duration> sleeps() { return List.copyOf(sleeps); }
}
Step 3 — RED: the half-implementation
Write a Countdown that prints the numbers but never sleeps:
public void run() {
for (int i = 3; i > 0; i--) {
writer.write(i + "\n");
}
writer.write("Go!\n");
}
Run it:
CountdownTest > writes each number, sleeps one second, then Go FAILED
org.opentest4j.AssertionFailedError: expected: <[PT1S, PT1S, PT1S]> but was: <[]>
The lines passed — but the spy recorded no sleeps. The test caught the omission. Note what it
did not need: a wall clock, three seconds, or verify-style frameworks. The spy’s recorded
list is the verification.
Step 4 — GREEN: add the sleep
public void run() {
for (int i = 3; i > 0; i--) {
writer.write(i + "\n");
sleeper.sleep(Duration.ofSeconds(1));
}
writer.write("Go!\n");
}
Run: GREEN — in milliseconds, on every machine, deterministically.
Vocabulary — because interviews love it
| Double | What it does | Here |
|---|---|---|
| Fake | working but simplified implementation | RecordingWriter collects output |
| Spy | records calls for later assertions | RecordingSleeper records durations |
| Stub | returns canned answers | (later, when method returns values) |
| Mock | asserts its own calls (verification) | libraries like Mockito add convenience |
Your two classes are a fake + a spy; Mockito just automates the same idea.
REFACTOR — the assertion IS the order
The assertion is implicit but real: the expected Lists are ordered, so the test proves
write → sleep → write → sleep → write → sleep → Go! — exactly the contract, without one second
of slow code.
Java-specific notes
Duration.ofSeconds(1)—java.time.Durationis the typed “one second”. The spy asserts on it, so the test catches someone passingDuration.ofMillis(500).- The hand-rolled double lives inside the test class (
private static final class) — it’s private to the test and can’t leak into production code. - In real projects (Spring, JPA, HTTP), the seam is an interface like
UserRepositoryorHttpClient, and the double is a Mockito@Mock. Same shape you just built, one annotation instead of the class. Mockito adds verification (verify(dao).save(user)) which equals yourRecordingasserts.
Interview gold
“how would you test code that sleeps / talks to a database / calls an HTTP API?” → “inject the collaborator behind an interface, replace it with a recording double, assert on the calls”. That sentence paints the whole picture — the DI of chapter 11 meeting the double of this chapter.
Run it
gradle test --tests "mocking.CountdownTest"
Key takeaway: never test the slow/fragile thing — test the contract it participates in. Inject the seam, observe through it, assert the choreography. The RED you just saw failed on the omission of a sleep, something no number of “sleep for real” tests could catch quickly.
That closes the fundamentals arc. Next: 13 — concurrency — running checks in parallel without waiting on the caller’s thread.