Skip to main content
Back to Blog
Series Playlist

JVM Quality Architecture

Eight guides to JUnit architecture, Gradle test suites, Testcontainers, parallel isolation, virtual-thread behavior, and AI-generated test review.

Series Chapters (8)

Part 1 · Automation Tutorials

Structure JUnit 5 Tests Around Behavior, Not Classes

Under JUnit Platform a test class name is not a discovery filter — Gradle states outright that scanForTestClasses has no effect there — so the one-test-class-per-production-class convention has no mechanical support behind it. This guide covers what the engine actually builds, grouping by the precondition a set of assertions shares rather than by package, turning the container tree into readable report prose with IndicativeSentences, the way inherited @Test methods silently multiply across subclasses, and the point at which a group stops being a container and becomes its own suite.

Sep 26, 202612 min read
Part 2 · Automation Tutorials

Use JUnit Parameterized Tests Without Matrix Explosion

A parameterized test that runs 288 times reports its failures as a coordinate rather than a claim, and by default Jupiter throws away any column your method forgot to declare — silently, with the test still passing. This guide covers choosing rows by the rule each one defends rather than by crossing dimensions, switching argument count validation to strict, why a CSV value is quoted in the display name before it is converted, keeping the expected value out of production code, and the three signals that a table should have been two tests.

Sep 27, 202615 min read
Part 3 · Automation Tutorials

Debug JUnit Parallel-Execution Flakes

A test that passes alone and fails in the suite, with the failure moving between classes, is an ownership bug that parallel execution revealed rather than caused. This guide covers pinning the configuration so the flake reproduces, the documented reason your fork-join pool can run more tests at once than the parallelism you configured, a probe that shows which threads two failing tests were on, when the answer is @TempDir rather than a lock, what @ResourceLock's READ and READ_WRITE modes actually schedule, and the check that tells Jupiter's threads apart from Gradle's forked JVMs.

Sep 28, 202614 min read
Part 4 · Automation Tutorials

Model JVM Test Layers with Gradle Test Suites

When integration tests live in src/test/java behind a filename filter, nothing stops a slow dependency entering the fast feedback path — and under JUnit Platform the filename is not even a discovery mechanism. This guide covers what a JvmTestSuite creates for you, why only the built-in test suite inherits the production code's dependencies and what implementation project() does and does not bring with it, wiring each suite into check with an explicit ordering, keeping suite identity in the JUnit XML and the aggregation variant, and the three costs of the split.

Sep 29, 202613 min read
Part 5 · Automation Tutorials

Build Trustworthy Database Tests with Testcontainers

A disposable database guarantees availability and a known image; it says nothing about whether your schema matches production or which test owns which rows. This guide covers separating container lifetime from data isolation, the singleton pattern the JUnit 5 extension deliberately does not provide, running your real migration set rather than letting an ORM invent a schema, choosing between a schema, a rollback and an owned key range, what the jdbc:tc URL mode is actually for, and a concurrent-insert test that asserts SQLSTATE 23505 rather than a message.

Sep 30, 202617 min read
Part 6 · Automation Tutorials

Test Java Virtual-Thread Code for Pinning and Cancellation

A test that submits a thousand virtual-thread tasks and waits for them proves only that they ended. This guide measures the ten connections behind them instead: a probe that asserts the concurrency bound is reached and never exceeded, a 500 ms deadline that has to reach the wait for a connection, the Semaphore release bug that inflates permits on cancellation, JDK 27 structured concurrency timeouts, and a JFR RecordingStream check for the pinning that survives JEP 491.

Oct 1, 202617 min read
Part 7 · Automation Tutorials

Make JUnit Failures Useful in CI

A red CI job that prints AssertionFailedError and a line number forces the next engineer to rebuild the scenario by hand. Using a parameterized contract test for GET /orders/{id}, this guide covers why Gradle's LIFECYCLE console omits assertion messages, keeping timestamps and generated ids out of test names, path/expected/actual messages with Supplier and assertAll, TestReporter entries and sanitized file attachments that Gradle 9.4 now carries into HTML and XML, redacting before anything reaches stdout, and why mergeReruns can make a flaky failure read as an ordinary pass.

Oct 2, 202617 min read
Part 8 · AI in Testing

Review AI-Generated JUnit Tests Before They Raise Coverage

Two JUnit suites for the same pricing rule produce identical JaCoCo rows, and only one of them fails when the threshold or the rounding mode changes. This guide covers mapping each generated test to a clause of the ticket, BigDecimal oracle traps (the double constructor, scale-sensitive equals), why stubbing the class that owns the rule shows up as NO_COVERAGE and inflates PIT's test strength, which mutants PIT's current DEFAULTS and STRONGER groups actually generate, and how to seed by hand the RoundingMode mutant no PIT operator produces.

Oct 3, 202617 min read