Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration from enclosing class not discovered for @Nested test class when enclosing class is not annotated with @ContextConfiguration #31456

Open
sbrannen opened this issue Mar 23, 2023 · 11 comments
Assignees
Labels
in: test Issues in the test module status: pending-design-work Needs design work before any code can be developed type: bug A general bug
Milestone

Comments

@sbrannen
Copy link
Member

sbrannen commented Mar 23, 2023

Given the following application and test classes, the @Nested test class fails due to getMessage() returning "Hello!" instead of "Mocked!" resulting from the fact that the static nested @TestConfiguration class is only discovered for the top-level enclosing test class.

Specifically, the MergedContextConfiguration for TestConfigurationNestedTests contains classes = [example.Application, example.TestConfigurationNestedTests.TestConfig]; whereas, the MergedContextConfiguration for InnerTests contains only classes = [example.Application].

A cursory search for @TestConfiguration reveals that SpringBootTestContextBootstrapper.containsNonTestComponent(...) uses the INHERITED_ANNOTATIONS search strategy for merged annotations. Instead, it should likely need to make use of TestContextAnnotationUtils in order to provide proper support for @NestedTestConfiguration semantics (perhaps analogous to the use of TestContextAnnotationUtils.searchEnclosingClass(...) in MockitoContextCustomizerFactory.parseDefinitions(...)).

As a side note, if you uncomment @Import(TestConfig.class) both test classes will pass.

package example;

// imports...

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@Service
	public static class GreetingService {
		public String getMessage() {
			return "Hello!";
		}
	}
}
package example;

// imports...

@SpringBootTest
// @Import(TestConfig.class)
class TestConfigurationNestedTests {

	@Test
	void test(@Autowired GreetingService greetingService) {
		assertThat(greetingService.getMessage()).isEqualTo("Mocked!");
	}

	@Nested
	class InnerTests {

		@Test
		void test(@Autowired GreetingService greetingService) {
			assertThat(greetingService.getMessage()).isEqualTo("Mocked!");
		}
	}

	@TestConfiguration
	static class TestConfig {

		@MockBean
		GreetingService greetingService;

		@BeforeTestMethod
		void configureMock(BeforeTestMethodEvent event) {
			when(this.greetingService.getMessage()).thenReturn("Mocked!");
		}
	}

}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 23, 2023
@sbrannen
Copy link
Member Author

Actually, if you annotate TestConfig with @Configuration instead of @TestConfiguration, the same behavior is displayed. So it appears that the issue has a larger scope than I originally reported.

@vpavic
Copy link
Contributor

vpavic commented Mar 23, 2023

Looks similar to spring-projects/spring-boot#33317, potentially even a duplicate.

@sbrannen
Copy link
Member Author

Thanks for pointing out spring-projects/spring-boot#33317, @vpavic.

It's certainly related, but I wouldn't consider it a duplicate since different solutions will be applied in different places to address the two sets of issues.

@devikaachu
Copy link

uncommenting the @import(TestConfig.class) annotation on the top-level test class will work around the issue.

Additionally, you could consider refactoring your test code to avoid the use of nested @TestConfiguration classes, if possible. For example, you could move the TestConfig class to the top-level test class and use @beforeeach and @AfterEach methods to set up and tear down the mocked beans.

@wilkinsona
Copy link
Member

Thanks, @sbrannen. Boot calls org.springframework.test.context.support.AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses(Class<?>), passing in InnerTests and it fails to find TestConfig as a configuration class. I think we'd expected detectDefaultConfigurationClasses to handle this arrangement for us. If that's not the case, is there another API or SPI in the test framework that we should be using instead to avoid reinventing the wheel?

@sbrannen
Copy link
Member Author

sbrannen commented Apr 3, 2023

Boot calls AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses(), passing in InnerTests and it fails to find TestConfig as a configuration class.

That's correct. That utility method only supports finding static nested @Configuration classes for a given test class.

If that's not the case, is there another API or SPI in the test framework that we should be using instead to avoid reinventing the wheel?

As I mentioned in this issue's description, I believe you will need to make use of TestContextAnnotationUtils to support this use case.

The following two test classes demonstrate the difference in behavior between Spring Framework and Spring Boot.

@SpringJUnitConfig
class SpringFrameworkNestedTests {

	@Test
	void test(@Autowired String foo) {
		assertThat(foo).isEqualTo("bar");
	}

	@Nested
	class InnerTests {
		@Test
		void test(@Autowired String foo) {
			assertThat(foo).isEqualTo("bar");
		}
	}

	@Configuration
	static class TestConfig {
		@Bean
		String foo() {
			return "bar";
		}
	}

}
@SpringBootTest
class SpringBootNestedTests {

	@Test
	void test(@Autowired String foo) {
		assertThat(foo).isEqualTo("bar");
	}

	@Nested
	class InnerTests {
		@Test
		void test(@Autowired String foo) {
			assertThat(foo).isEqualTo("bar");
		}
	}

	@Configuration
	static class TestConfig {
		@Bean
		String foo() {
			return "bar";
		}
	}

}

InnerTests in SpringFrameworkNestedTests is successful; whereas, InnerTests in SpringBootNestedTests is not.

Thus, the difference appears to be in the behavior of SpringBootTestContextBootstrapper.

@sbrannen
Copy link
Member Author

sbrannen commented Apr 4, 2023

@snicoll
Copy link
Member

snicoll commented Apr 6, 2023

Thus, the difference appears to be in the behavior of SpringBootTestContextBootstrapper.

I think we agree on that. Andy wrote:

I think we'd expected detectDefaultConfigurationClasses to handle this arrangement for us. If that's not the case, is there another API or SPI in the test framework that we should be using instead to avoid reinventing the wheel?

So. Is there an API we could use? TestContextAnnotationUtils doesn't sound right to me as it is too low-level.

@sbrannen
Copy link
Member Author

sbrannen commented Apr 6, 2023

So. Is there an API we could use? TestContextAnnotationUtils doesn't sound right to me as it is too low-level.

TestContextAnnotationUtils is the only API we have for honoring @Nested and @NestedTestConfiguration semantics. Please see the Javadoc for details as well existing usage in Spring Framework and Spring Boot for examples.

If you run into any stumbling blocks, let me know, and I'll see if I can help.

@wilkinsona
Copy link
Member

Thanks, @sbrannen. I've opened #30310 in the hope that things can be improved on the Framework side. In the meantime, I'll see what we can do with TestContextAnnotationUtils but it does feel like reinventing the wheel to me.

@wilkinsona wilkinsona added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Apr 11, 2023
@philwebb philwebb changed the title @TestConfiguration from enclosing class not discovered for @Nested test class @TestConfiguration from enclosing class not discovered for @Nested test class Jun 16, 2023
@snicoll snicoll added the status: blocked An issue that's blocked on an external project change label Aug 4, 2023
@wilkinsona wilkinsona removed the status: blocked An issue that's blocked on an external project change label Aug 9, 2023
@wilkinsona
Copy link
Member

wilkinsona commented Oct 18, 2023

This looks like a bug in Spring Framework to me. The difference in behavior described in this comment isn't a difference between Spring Boot and Spring Framework but a difference in Spring Framework's behavior with and without @ContextConfiguration:

package com.example;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;

@SpringJUnitConfig
class SpringJUnitConfigNestedTests {

	@Test
	void test(@Autowired String foo) {
		assertThat(foo).isEqualTo("bar");
	}

	@Nested
	class InnerTests {

		@Test
		void test(@Autowired String foo) {
			assertThat(foo).isEqualTo("bar");
		}

	}

	@Configuration
	static class TestConfig {

		@Bean
		String foo() {
			return "bar";
		}

	}

}
package com.example;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
class SpringExtensionNestedTests {

	@Test
	void test(@Autowired String foo) {
		assertThat(foo).isEqualTo("bar");
	}

	@Nested
	class InnerTests {

		@Test
		void test(@Autowired String foo) {
			assertThat(foo).isEqualTo("bar");
		}

	}

	@Configuration
	static class TestConfig {

		@Bean
		String foo() {
			return "bar";
		}

	}

}

SpringJUnitConfigNestedTests passes because of the @ContextConfiguration meta-annotation and SpringExtensionNestedTests fails because of its absence. With @ContextConfiguration added to the original example or to SpringBootNestedTests they pass as well.

Without @ContextConfiguration, the bootstrapper calls buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate) which uses Collections.singletonList(new ContextConfigurationAttributes(testClass)) as the config attributes list. With @ContextConfiguration the bootstrapper uses the result of ContextLoaderUtils.resolveContextConfigurationAttributes(testClass) as the config attributes list. In the latter case the @Nested test class becomes the outer class allowing the @Configuration or @TestConfiguration that it encloses to be found.

@wilkinsona wilkinsona changed the title @TestConfiguration from enclosing class not discovered for @Nested test class Configuration from enclosing class not discovered for @Nested test class when enclosing class is not annotated with @ContextConfiguration Oct 18, 2023
@wilkinsona wilkinsona removed the type: bug A general bug label Oct 18, 2023
@bclozel bclozel transferred this issue from spring-projects/spring-boot Oct 18, 2023
@bclozel bclozel added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 19, 2023
@snicoll snicoll added in: test Issues in the test module type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 27, 2023
@snicoll snicoll added this to the 6.2.x milestone Dec 27, 2023
@sbrannen sbrannen self-assigned this Oct 18, 2024
@sbrannen sbrannen changed the title Configuration from enclosing class not discovered for @Nested test class when enclosing class is not annotated with @ContextConfiguration Configuration from enclosing class not discovered for @Nested test class when enclosing class is not annotated with @ContextConfiguration Oct 18, 2024
@sbrannen sbrannen removed this from the 6.2.x milestone Nov 19, 2024
@sbrannen sbrannen added this to the 6.2.1 milestone Nov 19, 2024
@sbrannen sbrannen added the status: pending-design-work Needs design work before any code can be developed label Dec 3, 2024
@sbrannen sbrannen modified the milestones: 6.2.1, 6.2.x Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test Issues in the test module status: pending-design-work Needs design work before any code can be developed type: bug A general bug
Projects
None yet
Development

No branches or pull requests

7 participants