When would you implement JUnit 5's BeforeEachCallback/AfterEachCallback extension interfaces instead of just writing @BeforeEach and @AfterEach methods in the test class, and what do you gain?
answer
- annotated method = local fixture; extension = cross-cutting
- no single-inheritance limit — compose many extensions
- callbacks wrap the class's own @BeforeEach/@AfterEach
- ExtensionContext: test metadata + scoped store
- @RegisterExtension = configurable middle ground
basics
~20 sUse callbacks when the setup is cross-cutting and reusable across many test classes, needs to wrap the tests' own setup, or must be composed with other extensions. Annotated methods are fine for fixture code specific to one class.
solid answer
~60 s`@BeforeEach`/`@AfterEach` are the right tool for fixture code that belongs to **one test class**: build the object under test, seed local data. They are read where they are used and need no indirection. Reach for `BeforeEachCallback`/`AfterEachCallback` when the behaviour is **cross-cutting infrastructure** — starting a stubbed HTTP server, opening and rolling back a transaction, freezing a clock, resetting a shared registry, capturing logs. The gains: - **Reuse without inheritance.** One extension applies to any class via `@ExtendWith` or a custom composed annotation; a base class can only be extended once and creates a fragile hierarchy. - **Correct wrapping.** Extension callbacks run outside the user's annotated methods, so the class's own `@BeforeEach` can rely on what the extension prepared, and extension cleanup runs after the class's `@AfterEach`. - **Composition.** Several extensions nest deterministically (before in registration order, after in reverse), which base classes cannot express. - **Access to the ExtensionContext** — the test method, tags, display name, and the store for scoped state. Rule of thumb: specific to this class → annotated method; needed by many classes → extension.
code
java · 12 lines@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({RollbackTransactionExtension.class, StubHttpServerExtension.class})
public @interface IntegrationTest {}
@IntegrationTest
class OrderServiceTest {
@BeforeEach
void seed() {
repo.save(new Customer("c-1")); // inserted INSIDE the extension's transaction
}
}go deeper
Say annotated methods are for setup local to one class and extensions are for reusable setup shared by many classes.
Add the single-inheritance argument, the wrapping position around the class's own lifecycle methods, and ExtensionContext access.
Discuss composition order, meta-annotations that bundle concerns, @RegisterExtension for configured extensions, and the readability cost of hidden state.
Treat it as test-architecture policy: which environmental concerns are owned centrally, how they compose, when extraction is justified, and how to keep test setup legible as the suite scales.
## Two ways to run code around a test JUnit Jupiter offers a user-facing lifecycle (`@BeforeAll`, `@BeforeEach`, `@AfterEach`, `@AfterAll` methods in the test class) and an extension lifecycle (`BeforeAllCallback`, `BeforeEachCallback`, `AfterEachCallback`, `AfterAllCallback` implemented by a separate class and registered on the test). They can run the same code; they differ in reuse, position and capability. ## What annotated methods are good at Fixture code that only makes sense for this class. `orderService = new OrderService(repo, clock);` reads best right next to the tests that use it. There is no registration, no indirection, no extra file; a newcomer sees the whole setup in one place. If exactly one test class needs it, an extension is over-engineering. ## What extensions are good at **Reuse across classes without inheritance.** The classic pre-JUnit-5 way to share setup was an abstract base test class. Java has single inheritance, so two orthogonal concerns (a database transaction and a stubbed HTTP server) cannot both be base classes; teams end up with a deep, brittle `AbstractIntegrationTest` hierarchy that every test drags along. Extensions are composed: `@ExtendWith({DatabaseExtension.class, MockServerExtension.class})`, or better, folded into a single meta-annotation like `@IntegrationTest` that carries both. Adding a concern later touches one annotation, not a hierarchy. **Position in the lifecycle.** Extension callbacks sit *outside* the annotated methods: `BeforeEachCallback` runs before the class's `@BeforeEach`, and `AfterEachCallback` after its `@AfterEach`. That ordering is exactly what infrastructure needs — the extension opens the transaction, the test class's own setup inserts fixture rows inside it, the extension rolls it back afterwards. A base-class `@BeforeEach` cannot guarantee that relationship as cleanly once inheritance and nesting are involved. **Deterministic composition.** Multiple extensions wrap like nested try/finally blocks: before callbacks in registration order, after callbacks in reverse, with ordering controllable via `@Order` on `@RegisterExtension` fields. Two base classes cannot express that at all. **Richer context.** A callback receives an `ExtensionContext`, which exposes the test class and method, display name, tags, the unique id, the parent context, and a namespaced store for state scoped to the test or the container. Annotated methods see only what the class already holds (they can inject `TestInfo`, but they cannot participate in scoped storage or read a parent context). **Access to the rest of the extension model.** Once you are writing an extension you can also implement parameter resolution, exception handling, conditional execution or test-instance post-processing in the same class, so the concern stays in one place. **Distribution.** Extensions ship in a library and can be auto-discovered; that is how Mockito's `MockitoExtension`, Spring's `SpringExtension` and Testcontainers' integrations work. Base classes cannot be published as reusable behaviour in the same way. ## The programmatic middle ground `@RegisterExtension` on an instance or static field gives you an extension configured in code: ```java @RegisterExtension static WireMockExtension server = WireMockExtension.newInstance().options(...).build(); ``` This keeps the reuse and lifecycle position of an extension while letting the test class pass parameters — a middle ground between a declarative `@ExtendWith` and hand-written setup. Static fields participate in the class-level (`BeforeAll`) callbacks; instance fields in the per-test ones. ## Costs to weigh Extensions add indirection: reading a test no longer shows all the setup. Overuse produces "magic" suites where nobody can explain what state a test starts in. Guidelines that keep it healthy: - Extract to an extension on the **third** duplication, not the first. - Give the extension a name that states its effect (`RollbackTransactionExtension`, not `DbHelper`). - Keep it single-purpose and side-effect-visible; document what it leaves in the store or injects. - Prefer a composed meta-annotation so a test class declares intent (`@IntegrationTest`) rather than a list of mechanisms. ## The decision in one line If the code is about *this* class's subject under test, write `@BeforeEach`. If it is about the *environment* many classes need, and especially if it must wrap those classes' own setup or compose with other environmental concerns, write an extension.
- Your team already shares setup through an AbstractIntegrationTest base class. What concretely improves if you migrate it to extensions?You stop paying the single-inheritance tax: orthogonal concerns like transactions, a stubbed HTTP server and a frozen clock become independently composable rather than fused into one hierarchy, and a class can opt into exactly what it needs. You also gain deterministic wrapping order around each test class's own @BeforeEach, plus access to the ExtensionContext and its scoped store.
- When is @RegisterExtension preferable to @ExtendWith?When the extension needs configuration the test class supplies, or when the test body needs a handle on it — for example a stub server whose port or mappings the test inspects. @ExtendWith is purely declarative and cannot pass parameters, whereas @RegisterExtension binds a field you construct yourself; static fields participate in class-level callbacks, instance fields in per-test ones.
saying these in an interview costs you the question
- Extracting an extension for setup used by a single test class
- Believing the class's @BeforeEach runs before the extension's BeforeEachCallback
- Claiming extensions are just a syntax variant of base classes, missing composition and wrapping
- Hiding so much state in extensions that a test's starting conditions become unreadable