Hi,
In Spring Boot 1.5.4, when we use a @MockBean on type with more than one bean but one with @Primary, the test fail.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = PrimaryMockBeanTest.TestConfig.class)
public class PrimaryMockBeanTest {
@Test
public void test() throws Exception {}
@Configuration
public static class TestConfig {
@MockBean
Date mockDate;
@Bean
@Primary
public Date a() {
return new Date();
}
@Bean
@Qualifier("b")
public Date b() {
return new Date();
}
}
}
Exception is
Caused by: java.lang.IllegalStateException: Unable to register mock bean java.util.Date expected a single matching bean to replace but found [a, b]
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.getBeanName(MockitoPostProcessor.java:242)
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.registerMock(MockitoPostProcessor.java:187)
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.register(MockitoPostProcessor.java:177)
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.postProcessBeanFactory(MockitoPostProcessor.java:147)
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.postProcessBeanFactory(MockitoPostProcessor.java:134)
I think this behavior is strange because I expected that @MockBean works like @Autowired : when a @Primary is define @MockBean select it.
We could consider using @Primary as a discriminator, however it's not essential as you can use name="a" to select the bean to mock:
@MockBean(name = "a")
+1
+1
To workaround this, I had to qualify with @Qualifier(...) all my injections in the main code... doing so the purpose of @Primary was wasted... =(
Hi @wilkinsona, I don't think your proposed solution really solves it. I created an example here to illustrate it: https://github.com/neiser/mockbean-and-primary
In particular look at the test SomeControllerTest here: https://github.com/neiser/mockbean-and-primary/blob/master/src/test/java/com/example/mockbeanandprimary/SomeControllerTest.java
Also @hugobaes might see an idea how to workaround it without putting @Qualifier everywhere in production code, which is indeed really ugly.
The point is that @MockBean ignores @Primary trait when trying to insert the mocked service, which makes testing large controllers with a lot of dependencies hard if one only wants to mock certain aspects. Furthermore, using the proposed solution by @wilkinsona, the mock is indeed inserted but has lost its @Primary trait, which makes it impossible to let possibly large and complex SomeController to be inserted via @Autowired into the test.
As @alexandreBaronZnk pointed out, @MockBean should behave as @Autowired.
@neiser Thanks for sharing your findings. My proposal above wasn't a solution but a workaround for the specific problem reported in this issue. This issue is still open and we do intend to do something, but we have no immediate plans (or the necessary time) to do anything in 2.0. If you'd like that to change, then we may be able to find time to review and merge a pull request in 2.0 should you be willing to open one.
@wilkinsona Thanks for your quick reply! I understand that your answer solves the original problem, but I guess it did not point out the ramifications of mocking a primary bean, which is that it looses its @Primary trait and thus is not properly auto-wired anymore. It basically makes @MockBean unusable in this use case and is IMHO a bug.
I'll see what I can do concerning a merge request. Can you point me to a place where @MockBean does the job of inserting/replacing existing beans? I should say that I'm not an expert in this, but I could imagine that I could get something working once I found the right spot. Any help appreciated!
MockitoPostProcessor is the place to start, particularly its createBeanDefinition(MockDefinition) method.
Closing in favor of PR #11066.
Most helpful comment
We could consider using
@Primaryas a discriminator, however it's not essential as you can usename="a"to select the bean to mock: