skip to content

A developer reports that in JUnit 5 their @BeforeEach method runs only once even though the factory method they wrote produced twenty executable test cases at runtime. Why does that happen, and how do you give each generated case its own fresh setup?

level: middleimportance: must knowfreq 32%

answer

  1. Callbacks bind to the generating method, not the generated cases
  2. @BeforeEach once, @BeforeAll/@AfterAll normal
  3. Platform listeners see each case; Jupiter extension callbacks do not
  4. Setup inside the Executable + try/finally
  5. One shared transaction / one shared mock across all cases

basics

~20 s

Dynamic tests are generated at runtime, so Jupiter's per-test callbacks bind to the generating method, not to each generated case: @BeforeEach and @AfterEach run once around the whole factory. Put setup and teardown inside each executable, typically via a shared wrapper helper.

solid answer

~60 s

Jupiter's `@BeforeEach`/`@AfterEach` — and the corresponding extension callbacks `BeforeEachCallback`/`AfterEachCallback` — are bound to the **test method** that generates the cases, because that method is what discovery saw. The generated cases do not exist at discovery time, so no per-case lifecycle hook can be registered for them. The documented behaviour is that lifecycle callbacks execute once, around the generating method as a whole. `@BeforeAll`/`@AfterAll` behave normally. Note the asymmetry that confuses people: *platform* listeners do see each generated case (which is why an IDE lists them individually), but *Jupiter extension* callbacks do not fire per case. The fix is to move setup and teardown into the executable itself: ```java dynamicTest(name, () -> { var fixture = newFixture(); // per-case setup try { assertSomething(fixture); } finally { fixture.close(); } // per-case teardown }); ``` Factor that into a helper so every generated case uses it. If you truly need Jupiter-managed per-case lifecycle — injected parameters, extension callbacks, transactional rollback per case — that is a signal to use an annotation-driven templated test instead of generating nodes.

code

java · 13 lines
java
@BeforeEach
void setUp() {
    repo = mock(Repository.class); // runs ONCE for the whole factory
}

@TestFactory
Stream<DynamicTest> cases() {
    return Stream.of("a", "b", "c")
        .map(id -> dynamicTest("loads " + id, () -> {
            service.load(id);
            verify(repo, times(1)).findById(id); // invocations accumulate across cases
        }));
}

go deeper

for a junior

State the rule plainly: callbacks run once around the generating method, so put per-case setup inside the lambda.

for a middle

Explain why — the cases do not exist at discovery time — and show the wrapper-helper pattern with try/finally.

for a senior

Cover the downstream consequences (shared mocks, one transaction, leaked resources), how to spot them in review, and when to switch to an annotation-driven templated test instead.

for a principal

Frame it as owning the isolation contract yourself: generated suites trade framework-managed lifecycle for runtime flexibility, so the team needs a house helper and a review rule rather than per-author improvisation.

## Why the callbacks do not fire per case Jupiter's lifecycle is wired during **discovery**. The engine scans classes, finds the test methods, and for each one registers the callbacks that must run before and after it: `@BeforeEach`, `@AfterEach`, and every registered extension implementing `BeforeEachCallback`/`AfterEachCallback`. Dynamic tests do not exist at that moment. They are ordinary Java objects that some code creates while the suite is already executing. There is no method to attach callbacks to, and nothing for discovery to have seen. So Jupiter's rule is simple and documented: **lifecycle callbacks apply to the generating method, not to the generated cases**. Concretely, in a class with one generating method producing twenty cases: ``` @BeforeAll @BeforeEach <-- once case 1 .. case 20 <-- twenty executions, no callbacks between them @AfterEach <-- once @AfterAll ``` The same holds for anything else keyed to a test method: per-test parameter resolution, per-test extension state, `@Timeout` semantics, `TestWatcher` events for each case. All of it sees one test. ## The confusing asymmetry The reason this surprises people is that IDEs *do* show twenty separate entries with individual pass/fail marks. That is real: each generated case is reported to the JUnit Platform as its own test node, producing start and finish events for `TestExecutionListener`s. So there are two different "lifecycles" in play — the platform's reporting lifecycle (per generated case) and Jupiter's extension lifecycle (per generating method). Only the first one is per case. ## Consequences you must plan for **Shared mutable state leaks between cases.** If the generating method built a service, a mock, or a list, all twenty cases mutate the same instance in sequence. Case 7 can pass only because case 3 ran, and reordering or filtering the generator changes the result. This is the classic dynamic-test bug. **Database state is not rolled back per case.** Frameworks that wrap each test method in a transaction wrap the *generating method*. Twenty cases share one transaction; the rollback happens at the end. Data written by case 1 is visible to case 20. **Mock verification accumulates.** A mock created once records invocations from every case, so `verify(mock, times(1))` in case 2 counts case 1's calls too. **A leaked resource stays leaked.** Nothing closes anything between cases. ## How to give each case fresh setup **1. Build the fixture inside the executable.** The simplest and most honest form: the lambda owns everything it touches. ```java dynamicTest("import " + file.getName(), () -> { Importer importer = new Importer(newInMemoryStore()); assertTrue(importer.importFrom(file).isSuccess()); }); ``` **2. Extract a wrapper helper** so setup/teardown is written once and cannot be forgotten: ```java private DynamicTest isolated(String name, ThrowingConsumer<Fixture> body) { return dynamicTest(name, () -> { Fixture f = Fixture.create(); try { body.accept(f); } finally { f.close(); } }); } ``` Every generated case then goes through the same lifecycle, and the `finally` guarantees teardown even when an assertion fails. **3. Keep shared state immutable.** Anything created once by the generator should be read-only: a parsed spec, a started container, a stateless service. Mutable per-case state belongs inside the lambda. **4. Give each case its own data namespace.** Unique keys, unique table rows, unique temp directories — so cases cannot collide even though they share a connection or transaction. **5. Reconsider the tool.** If what you actually want is Jupiter-managed per-case lifecycle — extension callbacks, injected parameters, per-case transactional rollback — then annotation-driven templated tests, which *are* discovered up front, give you all of it. Generated nodes are the right choice when the *shape* of the test set must be computed at runtime and you accept owning the setup yourself. ## How to spot the bug in review Red flags in a generating method: fields or local variables mutated inside the lambdas; a mock created outside the lambdas and verified inside; assertions that depend on how many previous cases ran; `@BeforeEach` in the same class that clears state the generated cases rely on. Any of these means the twenty cases are not independent, and the report — which shows them as twenty separate tests — is lying about their isolation. ## One-line summary Generated cases get platform-level *reporting* per case but Jupiter-level *lifecycle* only around the generator; if you want per-case setup, you write it into the executable.

  • If @BeforeEach runs only once, why does the IDE still show each generated case as a separate test with its own result?
    Because there are two different lifecycles. Each generated case is reported to the JUnit Platform as its own test node, so TestExecutionListeners — which is what IDEs and report writers use — receive start and finish events per case. Jupiter's extension lifecycle, which owns @BeforeEach/@AfterEach and the BeforeEachCallback/AfterEachCallback extension points, is wired at discovery and therefore only knows about the generating method.
  • Your test class rolls back a transaction per test. What happens to twenty generated cases hitting the database?
    They all run inside the single transaction opened for the generating method, so nothing is rolled back between them and case 20 sees rows written by case 1. Either make each case write uniquely-keyed data and clean it up in a finally block, or manage a transaction explicitly inside each executable rather than relying on the per-method rollback.

The generating method is one seat at the exam hall. The proctor sets up and clears that seat once, no matter how many questions the candidate answers while sitting in it.

saying these in an interview costs you the question

  • Expecting @BeforeEach to run before each generated case.
  • Concluding JUnit is broken rather than that callbacks bind to the generating method.
  • Sharing a mock or mutable fixture across generated cases and then asserting invocation counts.
  • Assuming a per-test transactional rollback isolates the generated cases.
  • Doing teardown after the assertion without try/finally, so a failing case leaks its resources.

context