Spring Boot 1.4.0.RELEASE
I am trying to use the new @MockBean annotation to inject two mocks of the same parameterized interface, each with a different type i.e.:
@MockBean
private IdentityProvider<PasswordIdentity> passwordIdentityProvider;
@MockBean
private IdentityProvider<Oauth2Identity> oauth2IdentityProvider;
When running this test with @SpringBootTest I get the following error:
java.lang.IllegalStateException: Duplicate mock definition [MockDefinition@1b68b9a4 name = '', classToMock = IdentityProvider, extraInterfaces = set[[empty]], answer = RETURNS_DEFAULTS, serializable = false, reset = AFTER]
at org.springframework.util.Assert.state(Assert.java:392)
at org.springframework.boot.test.mock.mockito.DefinitionsParser.addDefinition(DefinitionsParser.java:119)
at org.springframework.boot.test.mock.mockito.DefinitionsParser.parseMockBeanAnnotation(DefinitionsParser.java:97)
at org.springframework.boot.test.mock.mockito.DefinitionsParser.parseElement(DefinitionsParser.java:76)
at org.springframework.boot.test.mock.mockito.DefinitionsParser.access$000(DefinitionsParser.java:42)
at org.springframework.boot.test.mock.mockito.DefinitionsParser$1.doWith(DefinitionsParser.java:67)
at org.springframework.util.ReflectionUtils.doWithFields(ReflectionUtils.java:692)
at org.springframework.util.ReflectionUtils.doWithFields(ReflectionUtils.java:672)
at org.springframework.boot.test.mock.mockito.DefinitionsParser.parse(DefinitionsParser.java:62)
at org.springframework.boot.test.mock.mockito.MockitoContextCustomizerFactory.createContextCustomizer(MockitoContextCustomizerFactory.java:38)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.getContextCustomizers(AbstractTestContextBootstrapper.java:418)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:387)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildDefaultMergedContextConfiguration(AbstractTestContextBootstrapper.java:323)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:277)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildTestContext(AbstractTestContextBootstrapper.java:112)
at org.springframework.boot.test.context.SpringBootTestContextBootstrapper.buildTestContext(SpringBootTestContextBootstrapper.java:74)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:120)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:105)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTestContextManager(SpringJUnit4ClassRunner.java:152)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.<init>(SpringJUnit4ClassRunner.java:143)
at org.springframework.test.context.junit4.SpringRunner.<init>(SpringRunner.java:49)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:104)
at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:86)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:26)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:33)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:96)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:253)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Note that exposing two beans of these parameterized types works fine in the regular non-test Spring context i.e. the types are injected correctly by Spring.
In addition, I can create the mocks manually via Mockito, also correctly.
I had a quick look but I think we might need some updates to Spring's RootBeanDefinition in order to support this.
Currently I don't think we have a way to programmatically register a bean definition with a specific ResolvableType. What we effectively need is a RootBeanDefinition.setTargetResolvableType(...) method and an update to GenericTypeAwareAutowireCandidateResolver that uses it.
@jhoeller am I correct with this assumption? Would adding support be something we'd consider in a point release?
@rocketraman You may be able to work around this issue in the meantime by specifying the specific bean names to replace when you declare your mocks. Something like:
@MockBean(name = "passwordIdentityProvider")
private IdentityProvider<PasswordIdentity> passwordIdentityProvider;
@MockBean(name = "oauth2IdentityProvider")
private IdentityProvider<Oauth2Identity> oauth2IdentityProvider;
@philwebb Yes, I can confirm that workaround works (though I need to explicitly specify the value in the @Component annotation). Or one can just define the mocks as done pre-1.4 in a test configuration.
@rocketraman Thanks for raising the issue. I'd like to provide better support for this but it may take some time since changes to Spring Framework are likely required.
We could indeed consider some extra field on a bean definition that indicates a generic target type to specifically match instead of getting that information from the bean class or factory method. Looks doable for 4.3.3 still, simply sitting next to our existing target type field on a root bean definition and not used unless specifically set on registration.
Related Spring Framework Issue : https://jira.spring.io/browse/SPR-14580
Fix is here https://github.com/philwebb/spring-boot/tree/gh-6602, just waiting for SPR-14580 to get merged.
Most helpful comment
@rocketraman You may be able to work around this issue in the meantime by specifying the specific bean names to replace when you declare your mocks. Something like: