Open sbrannen opened 1 year ago
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.
Looks similar to spring-projects/spring-boot#33317, potentially even a duplicate.
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.
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.
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?
Boot calls
AnnotationConfigContextLoaderUtils.detectDefaultConfigurationClasses()
, passing inInnerTests
and it fails to findTestConfig
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
.
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.
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.
Thanks, @sbrannen. I've opened https://github.com/spring-projects/spring-framework/issues/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.
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.
Given the following application and test classes, the
@Nested
test class fails due togetMessage()
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
forTestConfigurationNestedTests
containsclasses = [example.Application, example.TestConfigurationNestedTests.TestConfig]
; whereas, theMergedContextConfiguration
forInnerTests
contains onlyclasses = [example.Application]
.A cursory search for
@TestConfiguration
reveals thatSpringBootTestContextBootstrapper.containsNonTestComponent(...)
uses theINHERITED_ANNOTATIONS
search strategy for merged annotations. Instead, it should likely need to make use ofTestContextAnnotationUtils
in order to provide proper support for@NestedTestConfiguration
semantics (perhaps analogous to the use ofTestContextAnnotationUtils.searchEnclosingClass(...)
inMockitoContextCustomizerFactory.parseDefinitions(...)
).As a side note, if you uncomment
@Import(TestConfig.class)
both test classes will pass.