Java Guild course · Reference card
Reference · testing
The loop, the syntax, and the three kinds of test in this submission.
| Step | What you do | How you know you did it right |
|---|---|---|
| RED | Write the smallest test for behaviour that does not exist. | It fails, and the message names the behaviour — not a typo, not a null fixture. |
| GREEN | Write the least code that passes. | The whole suite is green, not just the new test. |
| REFACTOR | Improve the design. Change no behaviour. | No assertion changed. If you edited an expectation, that was not a refactor. |
A red bar is not the goal. These are the reds that count:
| Failure | Verdict |
|---|---|
| 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 setUp | Bad — your fixture is broken, and you have learned nothing about the code. |
| Kind | Asserts | Green on arrival? | Files |
|---|---|---|---|
| Golden master (approval / characterisation) | What the program does | Yes, by construction | GoldenMasterTest golden-master.approved.txt |
| Characterisation unit test | What existing units do | Yes — the code already worked | BookTest |
| Driving unit test (real TDD) | What the code should do | No — must fail first | LibraryTest (exceptions) LibraryPersistenceTest |
“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.
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() { ... }
}
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
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.
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.
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
@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.
JUnit 5 User Guide
— §2 Writing Tests, and the
@TempDir section.