-
Notifications
You must be signed in to change notification settings - Fork 38.8k
Description
Overview
After I investigated #34576, I discussed the difference in behavior between @Autowired fields and @Autowired arguments in lifecycle and test methods within the JUnit team, and @marcphilipp reminded me that he introduced a new ExtensionContextScope feature in JUnit Jupiter 5.12 which could allow the SpringExtension to behave the same for @Autowired fields as it already does for @Autowired arguments in lifecycle and test methods. See #34576 (comment) for background information.
In fact, if a developer sets the ExtensionContextScope to TEST_METHOD — for example, by configuring the following configuration parameter as a JVM system property or in a junit-platform.properties file — the SpringExtension already supports dependency injection from the current, @Nested ApplicationContext in @Autowired fields in an enclosing class of the @Nested test class.
junit.jupiter.extensions.testinstantiation.extensioncontextscope.default=test_methodHowever, there are two scenarios that fail as of Spring Framework 6.2.12.
@TestConstructorconfiguration in@Nestedclass hierarchies.- Field injection for bean overrides (such as
@MockitoBean) in@Nestedclass hierarchies.
Examples
NOTE: For all examples used here, it is assumed that @TestInstance(Lifecycle.PER_METHOD) semantics are in effect (which is the default behavior in JUnit Jupiter).
The following nested test class hierarchy passes as-is.
@SpringJUnitConfig
@TestConstructor(autowireMode = ALL)
class ConstructorTests {
final String text;
ConstructorTests(String text) {
this.text = text;
}
@Test
void test() {
assertThat(text).isEqualTo("enigma");
}
@Nested
@TestConstructor(autowireMode = ANNOTATED)
class NestedTests {
final String text;
@Autowired
NestedTests(String text) {
this.text = text;
}
@Test
void test() {
assertThat(text).isEqualTo("enigma");
}
}
@Configuration
static class Config {
@Bean
String text() {
return "enigma";
}
}
}However, if the ExtensionContextScope is set to TEST_METHOD, the above test class hierarchy fails as follows. The SpringExtension fails to find the @TestConstructor(autowireMode = ALL) declaration on ConstructorTests and instead finds the @TestConstructor(autowireMode = ANNOTATED) declaration on NestedTests, due to the switch in the ExtensionContext provided by JUnit Jupiter to SpringExtension.supportsParameter(...).
org.junit.jupiter.api.extension.ParameterResolutionException:
No ParameterResolver registered for parameter [java.lang.String text] in constructor
[example.ConstructorTests(java.lang.String)].
Similarly, the following nested test class hierarchy also passes as-is.
@SpringJUnitConfig
class MockitoBeanTests {
@MockitoBean
ExampleService bean1;
@Test
void test() {
assertThat(Mockito.mockingDetails(bean1).isMock()).isTrue();
}
@Nested
class NestedTests {
@MockitoBean
ExampleService bean2;
@Test
void test() {
assertThat(Mockito.mockingDetails(bean1).isMock()).isTrue();
assertThat(Mockito.mockingDetails(bean2).isMock()).isTrue();
}
}
@Configuration
static class Config {
@Bean
ExampleService bean1() {
return () -> "Bean 1";
}
@Bean
ExampleService bean2() {
return () -> "Bean 2";
}
}
}
interface ExampleService {
String greeting();
}However, if the ExtensionContextScope is set to TEST_METHOD, the above test class hierarchy fails as follows. The BeanOverrideTestExecutionListener incorrectly attempts to inject the example.MockitoBeanTests$NestedTests.bean2 field using an instance of its enclosing class example.MockitoBeanTests as the target object.
org.springframework.beans.factory.BeanCreationException: Could not inject field 'example.ExampleService example.MockitoBeanTests$NestedTests.bean2'
at org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener.injectField(BeanOverrideTestExecutionListener.java:138)
at org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener.injectFields(BeanOverrideTestExecutionListener.java:127)
at org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener.prepareTestInstance(BeanOverrideTestExecutionListener.java:70)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:159)
Caused by: java.lang.IllegalArgumentException: Can not set example.ExampleService field example.MockitoBeanTests$NestedTests.bean2 to example.MockitoBeanTests
at java.base/java.lang.reflect.Field.set(Field.java:799)
at org.springframework.util.ReflectionUtils.setField(ReflectionUtils.java:651)
at org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener.injectField(BeanOverrideTestExecutionListener.java:135)
NOTE: The exception message generated by java.lang.reflect.Field.set() is actually incorrect. That wording should probably be something more like "Cannot set example.ExampleService field example.MockitoBeanTests$NestedTests.bean2 on target of type example.MockitoBeanTests".
Deliverables
- Support
TEST_METHODExtensionContextScopewith@TestConstructorconfiguration in@Nestedclass hierarchies.- Look up
@TestConstructorvia the declaring class of the constructor instead the current test class. - Fixed in Introduce
isAutowirableConstructor(Executable, PropertyProvider)inTestConstructorUtilsand deprecate existing variants #35676. - Tests to be added in conjunction with this issue.
- Look up
- Support
TEST_METHODExtensionContextScopewith field injection for bean overrides (such as@MockitoBean) in@Nestedclass hierarchies.- Revise
BeanOverrideTestExecutionListener.injectField()to look up fields to inject for the current test instance instead of for the current test class.
- Revise
Related Issues
- Provide guidance on using
@Nestedwithin test classes with complex inheritance and annotation arrangements #28466 - No transaction in progress for
@Nestedtest class #34576 - Introduce
isAutowirableConstructor(Executable, PropertyProvider)inTestConstructorUtilsand deprecate existing variants #35676 - Optionally provide test-scoped
ExtensionContextfor test-specific callbacks in extensions junit-team/junit-framework#3445 - Replace opt-in annotation with extension method junit-team/junit-framework#4062
- Introduce configuration parameter for default extension context scope junit-team/junit-framework#4064