Lesson 9 / 25

Dynamic Tests, Ordering and Parallel Execution

Generate tests at run time, control ordering and run tests in parallel safely.

More control over execution

Dynamic tests are generated at run time by a @TestFactory method returning a stream or collection of DynamicTest (and optionally DynamicContainer for grouping). They suit test cases discovered from data, such as one test per JSON fixture file. Unlike parameterised tests, dynamic tests do not get @BeforeEach and @AfterEach callbacks per dynamic test, only per factory method. Ordering: JUnit runs tests in a deterministic but intentionally non-obvious order; well-designed unit tests must not depend on order. When order genuinely matters (for example in a step-by-step integration scenario), use @TestMethodOrder(MethodOrderer.OrderAnnotation.class) with @Order(n), but prefer independent tests. Parallel execution speeds up large suites: enable it in junit-platform.properties with junit.jupiter.execution.parallel.enabled=true and choose the default mode (concurrent or same_thread). Tests that share resources (a file, a static field, a database table) need @ResourceLock or @Execution(SAME_THREAD), otherwise parallelism creates flaky failures.

A test factory and parallel configuration

One dynamic test per fixture file; parallel execution enabled for the suite.

class TaxRulesTest {

    @TestFactory
    Stream<DynamicTest> everyFixtureMatchesExpectedTax() throws IOException {
        Path fixtures = Path.of("src/test/resources/tax-cases");
        return Files.list(fixtures)
            .filter(p -> p.toString().endsWith(".json"))
            .map(p -> DynamicTest.dynamicTest(p.getFileName().toString(), () -> {
                TaxCase c = TaxCase.read(p);
                assertEquals(c.expectedTaxPaise(), TaxRules.taxFor(c.invoice()));
            }));
    }
}

// src/test/resources/junit-platform.properties
// junit.jupiter.execution.parallel.enabled = true
// junit.jupiter.execution.parallel.mode.default = concurrent
// junit.jupiter.execution.parallel.mode.classes.default = concurrent

@ResourceLock("global-clock")             // tests touching the same shared resource run one at a time
class ClockDependentTest { /* ... */ }

Order-dependent tests are a smell

If tests pass only in a certain order, one test is leaking state into another. Find the shared state (a static field, a database row, a file) and isolate it, rather than pinning the order.

Quick check: What do you need to make tests that share a resource safe under JUnit parallel execution?

  • Nothing; JUnit detects shared resources
  • Static fields
  • Synchronisation such as @ResourceLock or running them in the same thread
  • More assertions
Answer

Synchronisation such as @ResourceLock or running them in the same thread — Shared mutable resources must be declared so JUnit serialises access to them.