Diagnosing JUnit 5 Failures from @TestInstance(PER_CLASS) Shared State
Shared mutable state in JUnit 5 @TestInstance(PER_CLASS) classes can cause flaky, order‑dependent failures. This guide offers a concise diagnostic table, step‑by‑step checks, targeted fixes, and escalation criteria to isolate and resolve the issue.
16 Oct 2025, 04:40 UTC

Problem Overview
When a test class is annotated with @TestInstance(Lifecycle.PER_CLASS), JUnit creates a single instance of the class and re‑uses it for every test method. If the class holds mutable fields, those fields survive between tests, causing flaky failures or order‑dependent bugs. This guide walks you through recognizing the symptom, diagnosing the root cause, and applying the right fix.
Recognizable Condition
Typical symptoms:
- Tests fail intermittently or only when run together.
- Console output shows “unexpected value” or “stale data” coming from a field that should be reset.
- Running a single test passes, but the full suite fails.
Cause & Diagnostic Table
| Condition | Symptoms | Likely Cause | Fix |
|---|---|---|---|
Shared mutable field initialized in @BeforeAll or constructor | Same value seen in multiple tests | Field not reset between tests | Move initialization to @BeforeEach or use PER_METHOD |
| Test methods modify shared collections (List, Map) | Subsequent tests see altered contents | State leakage | Clone/clear collections in @BeforeEach or use local variables |
Parallel execution enabled (@Execution(ExecutionMode.CONCURRENT)) | Non‑deterministic failures | Race conditions on shared state | Disable concurrency for that class or isolate state |
Incorrect use of @BeforeEach/@AfterEach with PER_CLASS | Cleanup code never runs | Lifecycle mis‑match | Ensure cleanup is in @AfterEach or switch to PER_METHOD |
Ordered Diagnostic Checks
- Verify TestInstance Annotation
Inspect the class declaration. If it reads
@TestInstance(Lifecycle.PER_CLASS)or@TestInstance(PER_CLASS), proceed; otherwise, skip to step 5. - Inspect Field Initialization
Check constructors and
@BeforeAllmethods for field assignments. Mutable objects (lists, maps, custom state holders) are suspect.Example:
public class SampleTest { private List<String> shared = new ArrayList<>(); @BeforeAll void init() { shared.add("init"); } } - Check for State Modification in Test Methods
Search for mutations (add, remove, put) on the shared fields. Log the field values at the start of each test to see if they persist.
Command to run a single test and observe console output:
mvn test -Dtest=SampleTest - Assess Parallel Execution
Look for
@Execution(ExecutionMode.CONCURRENT)at the class level or in thesurefireconfiguration. If present, add a temporary override:@Execution(ExecutionMode.SAME_THREAD)Re‑run the suite. Consistent results confirm concurrency was a factor.
- Validate Lifecycle Usage
Ensure
@BeforeEachand@AfterEachare used when state must be reset. If cleanup is only in@AfterAll, it will run once per class and not protect subsequent tests.
Fixes Tied to Findings
- Switch to PER_METHOD
Replace the annotation:
@TestInstance(Lifecycle.PER_METHOD)JUnit will instantiate a new class for each test, automatically isolating state.
- Move Initialization to @BeforeEach
Example:
@BeforeEach void setup() { shared = new ArrayList<>(); } - Clone or Clear Collections
In each test, start with a fresh copy:
List<String> local = new ArrayList<>(shared); - Disable Parallel Execution for the Class
Add the following to the test class or
pom.xml:@Execution(ExecutionMode.SAME_THREAD) - Use @AfterEach for Cleanup
Ensure any mutable state is cleared:
@AfterEach void tearDown() { shared.clear(); }
Escalation Criteria
- If the above fixes do not eliminate failures, check the build pipeline for custom JUnit extensions that might alter lifecycle behavior.
- Consult the team if tests rely on shared state for performance reasons; refactor to use dependency injection or a test utility that guarantees isolation.
- When flaky failures persist despite isolation, file a bug in the test framework or the library under test.
Practical Verification Checklist
- Run the full suite:
mvn testand confirm all tests pass. - Run a single test method:
mvn test -Dtest=SampleTest#testOneand compare logs. - Check test order independence by adding
@TestMethodOrder(MethodOrderer.Random.class)and ensuring no failures.
Conclusion
Shared instance state is a subtle source of flaky JUnit 5 tests. By following the ordered checks and applying the fixes tied to each diagnosis, you can reliably isolate test methods, eliminate order‑dependent failures, and maintain a stable test suite.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.