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 https://github.com/spring-projects/spring-framework/issues/34576#issuecomment-3398025742 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_method
However, 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
- [x] Support
TEST_METHODExtensionContextScopewith@TestConstructorconfiguration in@Nestedclass hierarchies. - Look up
@TestConstructorvia the declaring class of the constructor instead the current test class. - Fixed in #35676.
- Tests to be added in conjunction with this issue.
- [x] 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.
Related Issues
-
28466
-
34576
-
35676
- https://github.com/junit-team/junit-framework/issues/3445
- https://github.com/junit-team/junit-framework/pull/4062
- https://github.com/junit-team/junit-framework/pull/4064
Comment From: github-actions[bot]
Fixed via d24a31d469a43e49c4100533d01e488c357a1d69