Skip to content

Week 4 - Testing, Logging, and the Definition-of-Done

Conceptual Core

"Done" is not "compiles." Done is four invariants:

  1. Tested at multiple levels (unit, integration, property-based).
  2. Logs are structured and queryable (JSON, with a trace ID).
  3. Errors carry context (wrap, don't swallow).
  4. 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), @Nested for grouping, @DisplayName for readable output. Lifecycle: @BeforeEach/@AfterEach (per test), @BeforeAll/@AfterAll (per class - needs @TestInstance(PER_CLASS) for instance methods). Extensions via @ExtendWith(MockitoExtension.class) etc.
  • @Tag for grouping: mark slow tests @Tag("slow"), run only fast tests in pre-commit via Surefire's groups/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 @InjectMocks into final fields, 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's JsonLayout. MDC (Mapped Diagnostic Context) for per-request fields (trace ID, user ID); set in a request filter, used implicitly by every log line. Never System.out; never e.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/
One command per intent. This is the seed of your hardening/ template.

Comments