Skip to content

JUnit Jupiter TEST_METHOD ExtensionContextScope is not fully supported #35680

@sbrannen

Description

@sbrannen

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_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

  • Support TEST_METHOD ExtensionContextScope with @TestConstructor configuration in @Nested class hierarchies.
  • 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

Metadata

Metadata

Assignees

Labels

in: testIssues in the test moduletype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions