Boot version: 2.3.6.RELEASE
Let's say that we have the following production classes:
package a;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoanOrdersApplication {
public static void main(String[] args) {
SpringApplication.run(LoanOrdersApplication.class, args);
}
}
A controller
package a.controller;
import a.order.LoanOrder;
import a.order.LoanOrderService;
import reactor.core.publisher.Mono;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/orders")
public class LoanOrderController {
private final LoanOrderService loanOrderService;
public LoanOrderController(LoanOrderService loanOrderService) {
this.loanOrderService = loanOrderService;
}
@GetMapping(path = "/{orderId}")
Mono<LoanOrder> findOrder(@PathVariable String orderId) {
return loanOrderService.findOrder(orderId);
}
}
A service
package a.order;
public interface LoanOrderService {
Mono<LoanOrder> findOrder(String orderId);
}
and the following test class:
package a;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class LoanOrdersApplicationTests {
@Test
void contextLoads() {
}
}
According to the documentation (https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing-spring-boot-applications-detecting-config) what will happen now is that
When testing Spring Boot applications, this is often not required. Spring Boot's @*Test annotations search for your primary configuration automatically whenever you do not explicitly define one.
and according to the javadocs
If no explicit classes are defined the test will look for nested @Configuration classes, before falling back to a @SpringBootConfiguration search.
Great, I can see in the logs that the proper configuration was found cause I haven't passed the configuration explicitly.
15:13:35.700 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [/home/foo/bar/target/classes/a/LoanOrdersApplication.class]
15:13:35.701 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration a.LoanOrdersApplication for test class a.LoanOrderControllerTest
Let's say that now I want to create a new test, where I want to test only the controller layer with mocked out services. I don't want to use test slices - I want to set up the Boot context without certain autoconfigurations (like Spring Data for example). I will create the controller by myself and also a mock of a service (I'm using @TestConfiguration
so that the test that checks the whole context against the LoanOrderApplication
doesn't pick my test configuration).
@SpringBootTest(classes = LoanOrderControllerTest.ControllerWithFakeConfig.class)
class LoanOrderControllerTest {
@Test
void contextLoads() {
}
@TestConfiguration(proxyBeanMethods = false)
@EnableAutoConfiguration
static class ControllerWithFakeConfig {
@Bean
LoanOrderService testLoanOrderService() {
return Mockito.mock(LoanOrderService.class);
}
@Bean
LoanOrderController testLoanOrderController(LoanOrderService loanOrderService) {
return new LoanOrderController(loanOrderService);
}
}
}
Running LoanOrderControllerTest
makes the build fail because there already is a LoanOrderController
.
java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:123)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:118)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83)
at org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener.prepareTestInstance(SpringBootDependencyInjectionTestExecutionListener.java:43)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:244)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:98)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$5(ClassBasedTestDescriptor.java:341)
...
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webHandler' defined in class path resource [org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration$EnableWebFluxConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration$EnableWebFluxConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'testLoanOrderController' method
pl.smarttesting.loanorders.controller.LoanOrderController#findOrder(String)
to {GET /orders/{orderId}}: There is already 'loanOrderController' bean method
pl.smarttesting.loanorders.controller.LoanOrderController#findOrder(String) mapped.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
... 66 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration$EnableWebFluxConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'testLoanOrderController' method
pl.smarttesting.loanorders.controller.LoanOrderController#findOrder(String)
to {GET /orders/{orderId}}: There is already 'loanOrderController' bean method
pl.smarttesting.loanorders.controller.LoanOrderController#findOrder(String) mapped.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1794)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:624)
at org.springframework.context.support.AbstractApplicationContext.getBeansOfType(AbstractApplicationContext.java:1251)
at org.springframework.beans.factory.BeanFactoryUtils.beansOfTypeIncludingAncestors(BeanFactoryUtils.java:378)
at org.springframework.web.reactive.DispatcherHandler.initStrategies(DispatcherHandler.java:116)
at org.springframework.web.reactive.DispatcherHandler.setApplicationContext(DispatcherHandler.java:111)
at org.springframework.context.support.ApplicationContextAwareProcessor.invokeAwareInterfaces(ApplicationContextAwareProcessor.java:123)
at org.springframework.context.support.ApplicationContextAwareProcessor.postProcessBeforeInitialization(ApplicationContextAwareProcessor.java:100)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:415)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
... 81 more
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'testLoanOrderController' method
pl.smarttesting.loanorders.controller.LoanOrderController#findOrder(String)
to {GET /orders/{orderId}}: There is already 'loanOrderController' bean method
pl.smarttesting.loanorders.controller.LoanOrderController#findOrder(String) mapped.
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping$MappingRegistry.validateMethodMapping(AbstractHandlerMethodMapping.java:507)
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping$MappingRegistry.register(AbstractHandlerMethodMapping.java:485)
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping.registerHandlerMethod(AbstractHandlerMethodMapping.java:243)
at org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping.registerHandlerMethod(RequestMappingHandlerMapping.java:272)
at org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping.registerHandlerMethod(RequestMappingHandlerMapping.java:59)
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping.lambda$detectHandlerMethods$1(AbstractHandlerMethodMapping.java:213)
at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:723)
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping.detectHandlerMethods(AbstractHandlerMethodMapping.java:211)
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping.initHandlerMethods(AbstractHandlerMethodMapping.java:189)
at org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping.afterPropertiesSet(AbstractHandlerMethodMapping.java:157)
at org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet(RequestMappingHandlerMapping.java:126)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1790)
... 97 more
What I see in test execution logs is that the LoanOrdersApplication
class is being resolved as a configuration to be used by the test
15:13:35.700 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [/home/foo/bar/target/classes/a/LoanOrdersApplication.class]
15:13:35.701 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration a.LoanOrdersApplication for test class a.LoanOrderControllerTest
But this is strange cause according to the javadocs
If no explicit classes are defined the test will look for nested @Configuration classes, before falling back to a @SpringBootConfiguration search.
I did pass the explicit Configuration
classes via the classes
argument so I would assume that my configuration class would be resolved and the main one wouldn't be.
Either I'm doing sth wrong or the documentation & javadocs are wrong or there's a bug somewhere.
cc @snicoll
Comment From: snicoll
TL;DR
@SpringBootTest(classes = LoanOrderControllerTest.ControllerWithFakeConfig.class)
still detected the @SpringBootConfiguration
rather than starting the context with only LoanOrderControllerTest.ControllerWithFakeConfig.class
Comment From: dreis2211
@marcingrzejszczak The javadoc of @TestConfiguration
is more specific about this:
/**
* ....Unlike regular {@code @Configuration} classes the use of
* {@code @TestConfiguration} does not prevent auto-detection of
* {@link SpringBootConfiguration @SpringBootConfiguration}.
*/
You can use @Configuration
to workaround your issue, I guess. But I agree - the docs on @SpringBootTest#classes
are a bit misleading and I stumbled upon this in the past as well.
Comment From: snicoll
@dreis2211 I am confused by the comment. The problem that's raised here is that specifying a classes
attribute with a @Configuration
does not prevent the auto-detection of @SpringBootConfiguration
.
Comment From: dreis2211
@snicoll isn't ControllerWithFakeConfig
using @TestConfiguration
? @Configuration
would actually prevent it.
Comment From: snicoll
@dreis2211 funny. Marcin pasted the wrong example (we were trying several options offline). Marcin can you double check and update the example please?
Comment From: marcingrzejszczak
Actually when I make it @Configuration
it works fine. However Stephane AFAIR you said that it didn't work for you.
Comment From: marcingrzejszczak
Ok let's close the issue for now cause it seems that things work fine.