Design a custom TestExecutionListener that seeds and cleans up per-test data. Which callbacks do you implement, how do you register it without breaking defaults, and what are the pitfalls?
answer
- implement TestExecutionListener + Ordered, override before/afterTestMethod
- not a bean -> getBean from testContext.getApplicationContext()
- state in TestContext attributes, never instance fields
- order >4000 to run inside the tx (auto rollback)
- register via @TestExecutionListeners MERGE_WITH_DEFAULTS or spring.factories
basics
~10 sImplement TestExecutionListener, override beforeTestMethod to seed and afterTestMethod to clean up, give it @Order after the transactional listener, and register it via @TestExecutionListeners(listeners=..., mergeMode=MERGE_WITH_DEFAULTS) so defaults keep working.
solid answer
~40 sI'd implement org.springframework.test.context.TestExecutionListener and override only the callbacks I need — beforeTestMethod to seed data and afterTestMethod to tidy up — leaving the rest as defaults. To reach Spring beans I pull them from testContext.getApplicationContext(), because the listener itself is not a managed bean and can't be @Autowired. For ordering I implement Ordered with a value greater than TransactionalTestExecutionListener's (~4000) so my writes happen inside the test's transaction and roll back automatically. I register it with @TestExecutionListeners(listeners = MyListener.class, mergeMode = MergeMode.MERGE_WITH_DEFAULTS) so DI, transactions, and @Sql still function. Pitfalls: listeners are instantiated once and are effectively stateless/shared, so I keep per-test state in TestContext attributes not instance fields; exceptions in beforeTestMethod abort the test; and I must be transaction-aware to avoid leaking data when tests are non-transactional.
code
java · 40 linesimport org.springframework.core.Ordered;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
public class SeedDataTestExecutionListener
implements TestExecutionListener, Ordered {
@Override
public int getOrder() {
// run AFTER TransactionalTestExecutionListener (~4000)
// so our writes are inside the test's transaction and roll back
return 4500;
}
@Override
public void beforeTestMethod(TestContext testContext) {
// The listener is NOT a Spring bean -> pull beans from the context
WidgetRepository repo = testContext.getApplicationContext()
.getBean(WidgetRepository.class);
Widget seeded = repo.save(new Widget("seed"));
// Per-test state goes in TestContext attributes, never in fields
testContext.setAttribute("seededId", seeded.getId());
}
@Override
public void afterTestMethod(TestContext testContext) {
Object id = testContext.getAttribute("seededId");
if (id != null) {
testContext.getApplicationContext()
.getBean(WidgetRepository.class)
.deleteById((Long) id); // idempotent cleanup
}
}
}
// Usage:
// @SpringBootTest
// @TestExecutionListeners(listeners = SeedDataTestExecutionListener.class,
// mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
// class WidgetServiceTest { ... }go deeper
Know a custom listener implements TestExecutionListener and is registered via @TestExecutionListeners.
Pick before/afterTestMethod and remember MERGE_WITH_DEFAULTS.
Get beans from the TestContext and order the listener around the transaction.
Handle shared-instance statelessness, spring.factories registration, and transaction-aware cleanup design.
## Skeleton ```java public class SeedDataTestExecutionListener implements TestExecutionListener, Ordered { @Override public int getOrder() { return 4500; // after TransactionalTestExecutionListener (~4000) } @Override public void beforeTestMethod(TestContext testContext) { var ctx = testContext.getApplicationContext(); var repo = ctx.getBean(WidgetRepository.class); Widget w = repo.save(new Widget("seed")); // stash state for later callbacks in the TestContext, NOT a field testContext.setAttribute("seededId", w.getId()); } @Override public void afterTestMethod(TestContext testContext) { Object id = testContext.getAttribute("seededId"); if (id != null) { testContext.getApplicationContext() .getBean(WidgetRepository.class) .deleteById((Long) id); } } } ``` Register it: ```java @SpringBootTest @TestExecutionListeners( listeners = SeedDataTestExecutionListener.class, mergeMode = MergeMode.MERGE_WITH_DEFAULTS) class WidgetServiceTest { /* ... */ } ``` ## Key design decisions ### 1. Which callbacks Override only what you need — all methods are `default`. For per-test data use `beforeTestMethod`/`afterTestMethod`. If you only care about the pass/fail of the body, use `afterTestExecution` and read `testContext.getTestException()`. ### 2. Getting dependencies A `TestExecutionListener` is **not a Spring bean** — it's instantiated by the framework via a no-arg constructor. You therefore cannot `@Autowired` into it. Obtain collaborators from `testContext.getApplicationContext().getBean(...)`. The context is the same cached context the test uses. ### 3. Ordering vs the transaction Decide whether your writes should live inside the test's transaction: - **Inside (auto rollback):** order **> 4000** so `TransactionalTestExecutionListener` has already opened the tx. No manual cleanup needed if the test is `@Transactional`. - **Outside (survives across methods, or the test isn't transactional):** order accordingly and clean up explicitly in `afterTestMethod`. ### 4. State must not live in fields Spring creates **one** listener instance and reuses it for the whole class (and it may be shared). Storing per-test state in instance fields is a concurrency/leakage bug. Use `TestContext.setAttribute/getAttribute`, which is scoped to the current test context. ### 5. Registration options - `@TestExecutionListeners(..., mergeMode = MERGE_WITH_DEFAULTS)` on the class/base class — explicit and local. - Global auto-registration via `META-INF/spring.factories` under the `org.springframework.test.context.TestExecutionListener` key — applies everywhere without annotations (good for a shared test module). ## Pitfalls checklist - Forgetting `MERGE_WITH_DEFAULTS` -> `@Autowired`/`@Transactional`/`@Sql` break. - `@Autowired` in the listener -> always null; use `getBean`. - Instance fields for per-test state -> leakage; use TestContext attributes. - Exceptions in `beforeTestMethod` abort the test and may skip parts of setup — make it robust. - `after*` callbacks run in **reverse** listener order, and afterTestMethod still runs even if the test failed — write idempotent cleanup. - Non-transactional tests: your data won't roll back automatically; clean up explicitly. - Assuming `prepareTestInstance` is per-method — with `@TestInstance(PER_CLASS)` it isn't. ## When to prefer alternatives For pure SQL fixtures prefer `@Sql`; for event capture use `@ApplicationEvents`; write a custom listener only when you need cross-cutting behaviour those built-ins don't cover (metrics, external system reset, tenant setup).
- Why can't you @Autowired dependencies directly into your custom listener?The listener is instantiated by the TestContext framework via a no-arg constructor and is not a Spring-managed bean, so autowiring never happens; fetch beans from testContext.getApplicationContext().
- Where should per-test state live between beforeTestMethod and afterTestMethod, and why?In TestContext attributes (setAttribute/getAttribute). The listener is a single shared instance, so instance fields would leak or race across tests.
- How would you register a custom listener application-wide without annotating every test?Add it under the org.springframework.test.context.TestExecutionListener key in META-INF/spring.factories so it auto-registers among the defaults.
saying these in an interview costs you the question
- @Autowiring the listener and expecting non-null beans
- Storing per-test state in listener instance fields
- Forgetting mergeMode and silently disabling DI/Tx/Sql
- Assuming afterTestMethod is skipped when the test fails (it still runs)