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.

  1. @TestConstructor configuration in @Nested class hierarchies.
  2. Field injection for bean overrides (such as @MockitoBean) in @Nested class 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_METHOD ExtensionContextScope with @TestConstructor configuration in @Nested class hierarchies.
  • Look up @TestConstructor via 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_METHOD ExtensionContextScope with field injection for bean overrides (such as @MockitoBean) in @Nested class 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