How does constructor injection of beans into a JUnit 5 test class work with SpringExtension, and what are its constraints?
answer
- SpringExtension implements ParameterResolver
- Constructor params => final fields, no @Autowired fields
- @Autowired/@Qualifier/@Value signals Spring to resolve
- PER_METHOD => constructor per test; PER_CLASS => once
- Missing bean => NoSuchBeanDefinitionException
basics
~20 sBecause SpringExtension is also a JUnit 5 ParameterResolver, you can declare beans as constructor parameters of the test class and Spring injects them when it creates the test instance — no @Autowired fields needed. Add @Autowired on the constructor to disambiguate from other resolvers if required.
solid answer
~40 sSpringExtension implements JUnit 5's ParameterResolver, so it can supply the test class's constructor arguments as Spring beans. You write a normal constructor taking the beans you need; Spring resolves each parameter from the ApplicationContext. This gives you final fields and immutable test setup instead of @Autowired field injection. Key constraints: JUnit 5 shares one test instance construction per test method by default (PER_METHOD lifecycle), so the constructor runs per test. If another ParameterResolver competes, annotate the constructor (or the specific parameter) with @Autowired so SpringExtension knows to resolve it — Spring treats @Autowired as the signal. You can also put @Qualifier/@Value on individual parameters. With @TestInstance(PER_CLASS) the constructor runs once per class.
code
java · 19 lines@SpringJUnitConfig(AppConfig.class)
class PricingServiceTest {
private final PricingService pricing;
private final TaxRates rates;
// @Autowired disambiguates when other ParameterResolvers are present
@Autowired
PricingServiceTest(PricingService pricing,
@Qualifier("eu") TaxRates rates) {
this.pricing = pricing;
this.rates = rates;
}
@Test
void appliesTax() {
assertThat(pricing.priceWithTax(100, rates)).isEqualTo(120);
}
}go deeper
Know you can pass beans as constructor arguments to the test class and Spring injects them.
Explain the ParameterResolver mechanism and when @Autowired/@Qualifier on the constructor is needed.
Relate PER_METHOD/PER_CLASS lifecycle to constructor invocation and separate that from context caching; handle @Nested and optional deps.
Reason about resolver-ambiguity rules across multiple extensions and trade-offs of constructor vs field injection at scale in a test suite.
## The mechanism JUnit 5 lets extensions resolve **parameters** for constructors and methods through the `ParameterResolver` SPI. `SpringExtension` implements `ParameterResolver`, so anywhere JUnit 5 asks 'who can supply this parameter?', Spring can answer with a bean from the test's `ApplicationContext`. Applied to the **test class constructor**, this means you can inject collaborators as constructor arguments instead of using `@Autowired` fields: ```java @SpringJUnitConfig(AppConfig.class) class BillingServiceTest { private final BillingService billing; BillingServiceTest(BillingService billing) { this.billing = billing; } } ``` Spring resolves `BillingService` from the context and passes it in. Benefits: `final` fields, no reflection-based field mutation, clearer dependencies, and the same immutable style you use in production constructor injection. ## Disambiguation with @Autowired JUnit 5 forbids **two** resolvers both claiming the same parameter. If you have other extensions (e.g. `MockitoExtension`) that also resolve parameters, Spring needs a clear signal. The rule Spring uses: `SpringExtension` will resolve a constructor/parameter **only if** either (a) the parameter's declaring executable (or the parameter itself) is annotated with `@Autowired`, `@Qualifier`, or `@Value`, or (b) `SpringExtension` is the *only* resolver capable of handling it. So the safe, explicit form is: ```java @Autowired BillingServiceTest(BillingService billing) { ... } ``` You can also refine individual parameters: ```java MyTest(@Qualifier("primary") DataSource ds, @Value("${app.name}") String name) { ... } ``` ## Lifecycle interaction - Default JUnit 5 lifecycle is `@TestInstance(Lifecycle.PER_METHOD)`: a **new** test instance (and thus a new constructor call) per `@Test` method. Constructor injection therefore happens once per test method. - `@TestInstance(Lifecycle.PER_CLASS)`: one instance for the whole class, constructor runs once. Useful with non-static `@BeforeAll`. - The **ApplicationContext itself is cached** independently of instance lifecycle — the beans injected are the same singletons across methods (unless scope/dirties say otherwise). ## Constraints & gotchas - You **cannot mix** field and constructor injection for the *same* dependency meaningfully; pick constructor injection for the collaborators you want as final fields, `@Autowired` fields for the rest. - If a required bean is missing from the context, construction fails with `NoSuchBeanDefinitionException` — surfaced as a test initialization error. - `@Nested` test classes: the outer instance is created first; inner-class constructors can also receive injected parameters. - Constructor injection into the test does not support `required=false` semantics as cleanly as fields; prefer `@Autowired(required=false)` on a field or `ObjectProvider` if optionality is needed. - This is distinct from **method** parameter injection (into `@Test`/`@BeforeEach` methods), which uses the same `ParameterResolver` but at method-invocation time. ## When to use Prefer constructor injection in tests when you want immutable, clearly-declared dependencies and are not fighting other parameter-resolving extensions. Fall back to `@Autowired` fields when you have many optional collaborators or heavy `@Nested` structures where constructor plumbing gets noisy.
- Why might you annotate the test constructor with @Autowired even though field @Autowired usually isn't needed on the constructor in production code?In tests, other JUnit 5 ParameterResolvers may be active. Spring only claims a constructor parameter unconditionally when it's the sole resolver; @Autowired (or @Qualifier/@Value) is the explicit signal that tells SpringExtension to resolve it, avoiding an ambiguous-resolver error.
- Does constructor injection change how often the ApplicationContext is created?No. Context creation and caching are governed by the configuration and @DirtiesContext, not by test-instance lifecycle. Constructor injection only affects when the test instance (and its constructor) runs; the injected beans are the same cached context's beans.
saying these in an interview costs you the question
- Believing test-class constructor injection requires a special Spring 'test constructor' feature rather than JUnit 5's ParameterResolver
- Assuming @Autowired on the test constructor is always mandatory (only needed to disambiguate)
- Thinking each @Test gets a fresh ApplicationContext because a new instance is constructed