skip to content

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?

level: principalimportance: should knowfreq 20%

answer

  1. implement TestExecutionListener + Ordered, override before/afterTestMethod
  2. not a bean -> getBean from testContext.getApplicationContext()
  3. state in TestContext attributes, never instance fields
  4. order >4000 to run inside the tx (auto rollback)
  5. register via @TestExecutionListeners MERGE_WITH_DEFAULTS or spring.factories

basics

~10 s

Implement 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 s

I'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 lines
java
import 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

for a junior

Know a custom listener implements TestExecutionListener and is registered via @TestExecutionListeners.

for a middle

Pick before/afterTestMethod and remember MERGE_WITH_DEFAULTS.

for a senior

Get beans from the TestContext and order the listener around the transaction.

for a principal

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)

context