How do you share test fixtures (test doubles, seed helpers) across multiple test classes using @TestConfiguration and @Import?
answer
- top-level fixture class + @Import per test
- top-level not auto-scanned (by design)
- compose with @Import({A,B}) or meta-annotation
- same imports => shared cached context
- keep fixtures in src/test, never src/main
basics
~10 sPut the fixture beans in a top-level class annotated @TestConfiguration, then add @Import(TestFixtures.class) to each test class that needs them. Because it's top-level, it isn't auto-scanned, so @Import is what wires it in.
solid answer
~40 sExtract the shared fakes/stubs and helper beans into a standalone top-level `@TestConfiguration` class (e.g. `IntegrationTestFixtures`). Since top-level `@TestConfiguration` is intentionally excluded from component scanning, you opt in per test with `@Import(IntegrationTestFixtures.class)`. This gives you one place to define test doubles and seed helpers, reused across many test classes, while still layering them on top of the real application context. You can compose several fixture configs with `@Import({A.class, B.class})`, or create a custom composed annotation (a meta-annotation bundling `@SpringBootTest` + `@Import`) to keep the boilerplate down. Because Spring caches contexts by their unique configuration, tests that share the exact same imports and properties reuse one cached ApplicationContext — keeping the suite fast.
code
java · 34 lines// src/test/java/.../fixtures/IntegrationTestFixtures.java
@TestConfiguration
public class IntegrationTestFixtures {
@Bean
RecordingEmailSender emailSender() { // test double reused everywhere
return new RecordingEmailSender();
}
@Bean
Clock clock() {
return Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
}
}
// A composed annotation so tests don't repeat the wiring
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@Import(IntegrationTestFixtures.class)
public @interface IntegrationTest { }
// Usage — clean and cache-friendly (identical config => one shared context)
@IntegrationTest
class CheckoutFlowTest {
@Autowired RecordingEmailSender email;
@Autowired CheckoutService checkout;
@Test
void sendsConfirmation() {
checkout.placeOrder(sampleOrder());
assertThat(email.sent()).hasSize(1);
}
}go deeper
Know the mechanical recipe: top-level @TestConfiguration + @Import on the test.
Explain why @Import is required, how to compose fixtures, and keep them in src/test.
Discuss context-cache impact of import combinations and state-leak risks in reused contexts.
Design a fixture module strategy (composed annotations, minimal cache-key variation) balancing reuse, isolation, and suite runtime across a large codebase.
## The problem A nested static `@TestConfiguration` is convenient but scoped to a single test class. Once several tests need the same fake `EmailSender`, a fixed `Clock`, or a seeded repository helper, copy-pasting nested configs is wasteful and drift-prone. ## The pattern 1. **Create a top-level `@TestConfiguration` class** holding the shared `@Bean` fixtures. It lives under `src/test/java`. 2. **`@Import` it** into each test that needs it. Because top-level `@TestConfiguration` is annotated with `@TestComponent` and thus **excluded from the normal component scan**, it will NOT load unless imported — this is deliberate, so fixtures don't leak into every context. ```java @TestConfiguration public class IntegrationTestFixtures { @Bean EmailSender emailSender() { return new RecordingEmailSender(); } @Bean Clock clock() { return Clock.fixed(Instant.EPOCH, ZoneOffset.UTC); } } @SpringBootTest @Import(IntegrationTestFixtures.class) class CheckoutIntegrationTest { /* ... */ } ``` ## Composing fixtures - `@Import({FixturesA.class, FixturesB.class})` merges multiple fixture configs. - Build a **custom composed annotation** to bundle everything: ```java @Target(TYPE) @Retention(RUNTIME) @SpringBootTest @Import({IntegrationTestFixtures.class, ClockFixtures.class}) public @interface IntegrationTest {} ``` Then a test is just `@IntegrationTest`. ## Context caching interaction (important) The Spring TestContext Framework **caches** an `ApplicationContext` keyed by its full configuration (config classes, imports, active profiles, properties, etc.). Tests that share the **exact same** set of `@Import`s, properties, and profiles reuse **one** cached context — a big speed win. Varying imports per test fragments the cache and slows the suite because each distinct combination builds (and holds in memory) its own context. So standardizing on a small number of shared fixture bundles is both cleaner and faster. ## Overriding real beans via imported fixtures If a fixture bean has the **same name/type** as a production bean, you're overriding — which requires `spring.main.allow-bean-definition-overriding=true` (disabled by default since Boot 2.1), or you'll get `BeanDefinitionOverrideException`. To *add* rather than *override*, prefer distinct bean names/types, or use `@MockBean`/`@TestBean` for replacement semantics. ## Gotchas - Forgetting `@Import` — the fixture silently never loads. - Putting fixtures in `src/main` — they'd ship to production and get scanned into the real context. - Fragmenting the context cache with slightly different imports across tests. - Fixtures that carry mutable state (recording spies) must be reset between tests or context reuse leaks state — use `@DirtiesContext` sparingly, or reset in `@BeforeEach`. ## When to use Whenever the same test doubles or seed helpers appear in more than one or two test classes. For a single test, a nested static `@TestConfiguration` is simpler.
- Why not just put the fixture beans in src/main so they're always available?They'd be packaged into the production jar and picked up by the real component scan, polluting or breaking the production context with test doubles. Fixtures belong in src/test, imported explicitly.
- How does varying @Import across tests affect suite performance?Each distinct configuration key builds and caches its own ApplicationContext. Many one-off import combinations fragment the context cache, causing more context builds and higher memory use. Standardizing shared bundles maximizes reuse.
saying these in an interview costs you the question
- Expecting a top-level @TestConfiguration to load without @Import.
- Placing test fixtures under src/main.
- Ignoring that stateful fixtures leak across a cached/reused context.
- Not realizing same-config tests share one context.