Learn Java with Tests

A TDD course for Java, in the spirit of Learn Go with Tests.

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.Duration is the typed “one second”. The spy asserts on it, so the test catches someone passing Duration.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 UserRepository or HttpClient, 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 your Recording asserts.

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.