Java Guild course · Reference card

Reference · testing

TDD Cycle & JUnit 5

The loop, the syntax, and the three kinds of test in this submission.

The loop

StepWhat you doHow you know you did it right
REDWrite the smallest test for behaviour that does not exist.It fails, and the message names the behaviour — not a typo, not a null fixture.
GREENWrite the least code that passes.The whole suite is green, not just the new test.
REFACTORImprove the design. Change no behaviour.No assertion changed. If you edited an expectation, that was not a refactor.
Red for the right reason

A red bar is not the goal. These are the reds that count:

FailureVerdict
expected: <AlreadyBorrowedException>
but was: <java.io.IOException>
Good — names the exact design defect.
expected: <Coming Home; A Novel>
but was: <Coming Home>
Good — shows the data being lost.
cannot find symbol:
class LibraryStorageException
Acceptable — the legitimate first red for a type that does not exist yet. Resolve it to a behavioural red next.
NullPointerException in setUpBad — your fixture is broken, and you have learned nothing about the code.

The three kinds of test in this submission

KindAssertsGreen on arrival?Files
Golden master
(approval / characterisation)
What the program doesYes, by constructionGoldenMasterTest
golden-master.approved.txt
Characterisation unit testWhat existing units doYes — the code already workedBookTest
Driving unit test (real TDD)What the code should doNo — must fail firstLibraryTest (exceptions)
LibraryPersistenceTest
Say this precisely in interview

The existing behaviour was characterised — those tests were green when written. The new behaviour was driven test-first, and you can see the red in the commit sequence.” Claiming full TDD over a pre-existing implementation is an overclaim anyone can check.

JUnit 5 syntax you need here

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("A library")
class LibraryTest {

    @Test
    @DisplayName("refuses to borrow a book that is already out")
    void refusesToBorrowABookAlreadyOut() { ... }

    @Nested @DisplayName("when borrowed")
    class WhenBorrowed { ... }

    @TempDir Path directory;          // fresh dir per test, auto-deleted

    @BeforeEach void setUp() { ... }
}

Assertions

assertEquals(expected, actual);           // note the order
assertTrue(cond, "message shown on failure");
assertFalse(cond);
assertThrows(BookNotFoundException.class, () -> library.borrowBook("x"));
assertAll(() -> assertEquals(...), () -> assertEquals(...));   // all, not first-fail
The pattern that carries this submission
AlreadyBorrowedException thrown = assertThrows(AlreadyBorrowedException.class,
        () -> library.borrowBook("Dune"));
assertEquals("'Dune' is already borrowed", thrown.getMessage());

assertThrows returns the exception. Asserting the message as well as the type is what proves you pulled the two conflated cases apart — type alone would not.

Capturing console I/O

ByteArrayOutputStream captured = new ByteArrayOutputStream();
new LibraryConsole(
    new Library(),
    new Scanner(new ByteArrayInputStream(input.getBytes(UTF_8))),
    new PrintStream(captured, true, UTF_8)
).run();
return captured.toString(UTF_8);

Always name the charset. PrintStream(OutputStream, boolean) uses the platform default, so a test can pass on your Mac and fail on a build server.

Running it

export JAVA_HOME=$(/usr/libexec/java_home -v 25)
~/.local/maven/bin/mvn test                          # everything
~/.local/maven/bin/mvn test -Dtest=LibraryTest       # one class
~/.local/maven/bin/mvn test -Dtest=LibraryTest#refusesToBorrowAnUnknownBook
Surefire and @Nested

Surefire mis-attributes nested tests in its per-class summary. Observed on this project:

Tests run: 5 ... in BookTest$WhenReturned   // actually has 2
Tests run: 2 ... in BookTest$WhenBorrowed
Tests run: 0 ... in BookTest                // actually has 3
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0   // the total is correct

Trust the total. IDEs render the tree correctly.

Primary source

JUnit 5 User Guide — §2 Writing Tests, and the @TempDir section.