skip to content

A JUnit 5 test class relies on a fixed execution order of its methods, and the team now wants to enable Jupiter's parallel execution for the whole suite. What actually breaks, and how do you decide between preserving the ordering and removing the dependency between those tests?

level: principalimportance: should knowfreq 22%

answer

  1. Ordering = start order, not completion, under parallelism
  2. @Execution(SAME_THREAD) on the ordered class
  3. @ResourceLock for global shared state
  4. MethodOrderer.getDefaultExecutionMode = implicit safety net
  5. Expensive fixture -> shared read-only resource, not predecessor test

basics

~20 s

Under parallel execution an orderer fixes the order tests are started, not that one finishes before the next begins. Pin ordered classes to a single thread (@Execution(SAME_THREAD)) or guard shared state with @ResourceLock — then decide whether the sequence is genuinely worth losing parallelism and single-test reruns.

solid answer

~60 s

**What breaks.** A `MethodOrderer` decides the sequence in which methods are handed to the executor. With parallel execution enabled, methods can then overlap, so "step 2 after step 1" becomes "step 2 started after step 1 started". Any test that reads what a previous test wrote can now see partial or missing state, and failures become non-deterministic. Jupiter softens this — `MethodOrderer` can declare a default execution mode of `SAME_THREAD`, which is why an `@Order`-ed class does not immediately explode — but relying on that implicitly is fragile; make it explicit with `@Execution(ExecutionMode.SAME_THREAD)` on the class, and `@ResourceLock` for state shared with *other* classes. **How I decide.** Ordering that is merely narrative (every test would pass alone) is free — keep it. Ordering that carries a real dependency costs single-test runs, meaningful rerun-failed, clean failure attribution, and parallel speedup inside that class. I keep it only where re-establishing the fixture per test is genuinely prohibitive — long UI or migration scenarios — and I contain it: one clearly named scenario class, pinned to one thread, everything else independent. Elsewhere the fix is to make the expensive setup a shared read-only resource with per-test data, not a predecessor test.

code

java · 14 lines
java
@Execution(ExecutionMode.SAME_THREAD)
@ResourceLock(value = "migration-schema", mode = ResourceAccessMode.READ_WRITE)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Tag("scenario")
class SchemaMigrationScenarioTest {

    @Order(10)
    @Test
    void migratesBaseline() { }

    @Order(20)
    @Test
    void migratesIncrement() { }
}

go deeper

for a junior

Say that parallel execution can overlap tests so an ordered class needs to be forced onto a single thread, and that depending on order is best avoided.

for a middle

Name the concrete levers — @Execution(SAME_THREAD), @ResourceLock — and explain that ordering only controls start order once execution is concurrent.

for a senior

Diagnose the failure mode, apply containment, and articulate what the dependency costs operationally: single-test runs, rerun-failed, failure attribution, throughput.

for a principal

Make it a strategy call: independence as the precondition for parallelism and sharding, ordered scenarios as a contained and tagged exception with a stated fixture-cost justification, expensive setup reframed as a shared read-only resource.

## The mechanics first Jupiter's parallel execution is opt-in (`junit.jupiter.execution.parallel.enabled`) with a default mode for classes and methods. Ordering and parallelism are orthogonal features that meet awkwardly: - A `MethodOrderer` sorts the method descriptors of a class *before* execution begins. - The executor then runs them according to the configured execution mode. If that mode is concurrent, sorting only determines **dispatch order**. Method 2 may start while method 1 is still inside its assertions. Everything an order-dependent test assumed — the row exists, the cache is warm, the file was written — becomes a race. Jupiter provides a hook: `MethodOrderer` declares `getDefaultExecutionMode()`, allowing an orderer to state that the methods it ordered should run on the same thread. That is why enabling parallelism does not instantly shred every `@Order`-ed class. But it is an *implicit* protection: it depends on the orderer in use, it is easy to lose when someone swaps orderers, and it says nothing about concurrency with tests in *other* classes. Treat it as a safety net, not a design. ## Making it explicit Two tools: **`@Execution(ExecutionMode.SAME_THREAD)`** on the ordered class forces its methods to run sequentially on one thread, regardless of the global default. The class still runs concurrently with other classes. **`@ResourceLock("someKey")`** declares that a test (or class) touches a shared resource, with `READ` or `READ_WRITE` access. Jupiter serialises writers against everything else holding the same key. This is the right tool when the shared thing is *global* — a system property, a fixed database table, a singleton, `System.out`. `Resources.SYSTEM_PROPERTIES` and friends are predefined keys for exactly these. Used together: pin the scenario class to one thread, and lock the external resource it mutates so no other class interferes. ## The real decision Mechanics are the easy half. The judgment call is whether the ordering should exist at all. Separate two things that look identical in the code: **Narrative ordering.** Tests are ordered so the report reads sensibly, but each would pass alone. This costs nothing — no thread pinning needed, parallelism intact. Keep it. **Dependency ordering.** Test N only passes because tests 1..N-1 ran. This is a design decision with a bill: - *Single-test runs stop working.* A developer debugging step 4 in the IDE cannot run step 4. - *Rerun-failed becomes useless.* CI reruns the failed method alone and it fails again for the wrong reason, or passes spuriously. - *Failure attribution collapses.* One real defect renders N tests red; the report no longer tells you how many things are broken. - *Parallelism is lost inside the class*, and if a resource lock is needed, partly lost across the suite. - *The class becomes change-hostile.* Inserting a step in the middle can invalidate every later step. Against that, the legitimate benefit is time: a scenario whose setup takes minutes (a full migration, a multi-step checkout through a real browser, a deployment) genuinely cannot be re-established per assertion. If the fixture cost per test exceeds the value of independence, ordering wins — but you should be able to say the number out loud. ## The middle path I prefer Most "we need ordering" claims are actually "we need an expensive fixture once". Those are different problems, and the second has a better answer: make the expensive thing a **shared resource**, not a **predecessor test**. - Start the container / server / schema once in `@BeforeAll`, or in a lifecycle-aware extension shared across classes. - Keep the shared state **read-only**; per-test mutable data is created by the test that needs it, with unique keys. - Clean up per test (transaction rollback, delete-by-key, throwaway namespace). Now the expensive setup is paid once, every test still passes alone, parallelism is intact, and ordering is free to be purely cosmetic. ## Containment when ordering stays When a genuine scenario class survives the analysis, contain it: 1. One class, named for the scenario, with a comment stating the sequence is load-bearing and why. 2. `@TestMethodOrder(MethodOrderer.OrderAnnotation.class)` with gapped `@Order` values. 3. `@Execution(ExecutionMode.SAME_THREAD)` explicitly, plus `@ResourceLock` for anything global it touches. 4. Consider making later steps *skip* rather than *fail* when an earlier step did not complete — an assumption on a flag turns a cascade of red into one red plus skips, which keeps the report honest. 5. Tag it (`@Tag("scenario")`) so it can be excluded from fast local runs and reported separately. And enforce the opposite default everywhere else: randomised ordering globally is the cheapest standing guard that no other class quietly grew a dependency. The strategic point is that test independence is not a purity preference — it is the precondition for parallelism, sharding, rerun-failed and trustworthy failure counts. Ordering is a local exception you pay for knowingly, not a suite-wide style.

  • Why prefer @Execution(SAME_THREAD) over relying on the orderer's own default execution mode?
    Because the orderer's default is an implicit property of the strategy you happened to choose. Swap the orderer, write a custom one, or reconfigure the global default and the protection silently disappears — with no compile error and only intermittent failures to warn you. An explicit @Execution on the class states the requirement where a reader will see it.
  • When is @ResourceLock the right tool instead of pinning to a single thread?
    When the contention is between different classes rather than between methods of one class — a system property, a fixed table, a port, a singleton mutated by several test classes. SAME_THREAD only serialises within the annotated container; a resource lock serialises everything that declares the same key across the run, with READ allowing concurrent readers and READ_WRITE excluding them.

An ordered class under parallel execution is like starting runners in seeded order and then declaring the finish order matches — it only holds if you make them run one at a time.

saying these in an interview costs you the question

  • Believing a MethodOrderer guarantees one test completes before the next starts when parallel execution is on.
  • Solving parallel flakiness by disabling parallelism suite-wide instead of pinning the one offending class.
  • Treating @Order as a fix for shared-state leakage rather than a symptom of it.
  • Assuming @Execution(SAME_THREAD) also protects against other classes touching the same external resource.
  • Arguing test ordering is always wrong, with no account of genuinely expensive end-to-end fixtures.

context