Week 4 - Testing, Logging, and the Definition-of-Done¶
Conceptual Core¶
"Done" is not "compiles." Done is four invariants:
- Tested at multiple levels (unit, integration, property-based).
- Logs are structured and queryable (JSON, with a trace ID).
- Errors carry context (wrap, don't swallow).
- The artifact is reproducible - another engineer can build and run with one command.
Month 1 ends when your build produces a JAR (or jlink image) that lands on someone else's machine and starts cleanly.
Mechanical Detail¶
- JUnit 5 (Jupiter):
@Test,@ParameterizedTest(with@ValueSource,@MethodSource,@CsvSource),@Nestedfor grouping,@DisplayNamefor readable output. Lifecycle:@BeforeEach/@AfterEach(per test),@BeforeAll/@AfterAll(per class - needs@TestInstance(PER_CLASS)for instance methods). Extensions via@ExtendWith(MockitoExtension.class)etc. @Tagfor grouping: mark slow tests@Tag("slow"), run only fast tests in pre-commit via Surefire'sgroups/excludedGroups.- AssertJ over Hamcrest over JUnit's bare
assertEquals- fluent, readable, great failure messages. Idioms:assertThat(list).containsExactly(a, b, c),.extracting(Person::name),.filteredOn(p -> p.age() > 18). - Mockito 5+: mock collaborators via constructor injection (Mockito 5 cannot
@InjectMocksintofinalfields, which records and well-written classes use). Never mock what you don't own -Connection,HttpClient, framework types. Wrap in your own interface, mock that. - Testcontainers for real dependencies:
@Container PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16"). Test cost: 3-5s spin-up; pays for itself in caught bugs that mocks would hide. In CI: Docker-in-Docker or a Testcontainers Cloud worker. - Property-based with jqwik:
@Property void roundTrips(@ForAll String input) { ... }. Shrinks failing cases to a minimal reproducer automatically. - Logging: SLF4J as the facade, Logback or Log4j2 as the backend. Structured JSON via
logstash-logback-encoder(Logback) or Log4j2'sJsonLayout. MDC (Mapped Diagnostic Context) for per-request fields (trace ID, user ID); set in a request filter, used implicitly by every log line. NeverSystem.out; nevere.printStackTrace().
The trap
catch (Exception e) { log.error("error", e); } without context is barely better than printStackTrace. Always include what you were doing: log.error("validating order {} for customer {}", orderId, customerId, e).
Lab¶
Take Week 3's CSV aggregator (or any small evaluator). Write:
- 10 unit tests (JUnit 5 + AssertJ).
- 3 parameterized tests covering edge cases (empty input, single row, malformed timestamp).
- 1 property-based test (@ForAll List<Event> events -> aggregator.process(events).size() <= events.size()).
- Structured JSON logging at boundaries (input received, output produced).
Idiomatic Drill¶
Configure ErrorProne or SpotBugs or IntelliJ's "Inspect Code" action in your build. Resolve every warning on your Month 1 codebase.
Production Hardening Slice¶
Produce a Makefile (or justfile):
build: mvn package
test: mvn test
run: java -jar target/app.jar
image: jlink --add-modules $(jdeps --print-module-deps target/app.jar) --output runtime/
hardening/ template.