When the same mock declaration (via @MockitoBean
) is indirectly referenced through multiple meta-annotation paths on a test class, Spring's BeanOverrideContextCustomizerFactory
reports a Duplicate BeanOverrideHandler
exception.
The framework treats the two paths to the same mock declaration as distinct, even though it should result in a single mock instance.
To Reproduce:
package org.springframework.test.context.bean.override;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@SuppressWarnings("javadoc")
public class BeanOverrideHandlerBug {
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Inherited
@MockitoBean(types = MockedService.class)
@ImportAutoConfiguration({ ServiceThatUsesMockedServiceAutoConfiguration.class })
public @interface WithServiceThatUsesMockedService {}
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Inherited
@WithServiceThatUsesMockedService
@ImportAutoConfiguration({ ServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration.class })
public @interface WithServiceThatUsesTheServiceThatUsesMockedService {}
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Inherited
//Having two paths to the same @MockitoBean causes failures
@WithServiceThatUsesMockedService
@WithServiceThatUsesTheServiceThatUsesMockedService
@ImportAutoConfiguration({ AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration.class })
public @interface WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceBad {}
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Inherited
@WithServiceThatUsesTheServiceThatUsesMockedService
@ImportAutoConfiguration({ AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration.class })
public @interface WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceGood {}
public static class MockedService {}
public static class ServiceThatUsesMockedService {
private final MockedService mockedService;
public ServiceThatUsesMockedService(final MockedService mockedService) {
this.mockedService = mockedService;
}
public MockedService getMockedService() {
return this.mockedService;
}
}
public static class ServiceThatUsesTheServiceThatUsesMockedService {
private final ServiceThatUsesMockedService serviceThatUsesMockedService;
public ServiceThatUsesTheServiceThatUsesMockedService(
final ServiceThatUsesMockedService serviceThatUsesMockedService) {
this.serviceThatUsesMockedService = serviceThatUsesMockedService;
}
public ServiceThatUsesMockedService getServiceThatUsesMockedService() {
return this.serviceThatUsesMockedService;
}
}
public static class AnotherServiceThatUsesTheServiceThatUsesMockedService {
private final ServiceThatUsesMockedService serviceThatUsesMockedService;
private final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService;
public AnotherServiceThatUsesTheServiceThatUsesMockedService(
final ServiceThatUsesMockedService serviceThatUsesMockedService,
final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService) {
this.serviceThatUsesMockedService = serviceThatUsesMockedService;
this.serviceThatUsesTheServiceThatUsesMockedService = serviceThatUsesTheServiceThatUsesMockedService;
}
public ServiceThatUsesMockedService getServiceThatUsesMockedService() {
return this.serviceThatUsesMockedService;
}
public ServiceThatUsesTheServiceThatUsesMockedService getServiceThatUsesTheServiceThatUsesMockedService() {
return this.serviceThatUsesTheServiceThatUsesMockedService;
}
}
@Configuration
public static class ServiceThatUsesMockedServiceAutoConfiguration {
private final MockedService mockedService;
public ServiceThatUsesMockedServiceAutoConfiguration(final MockedService mockedService) {
this.mockedService = mockedService;
}
@Bean
public ServiceThatUsesMockedService serviceThatUsesMockedService() {
return new ServiceThatUsesMockedService(this.mockedService);
}
}
@Configuration
public static class ServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration {
private final ServiceThatUsesMockedService serviceThatUsesMockedService;
public ServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration(
final ServiceThatUsesMockedService serviceThatUsesMockedService) {
this.serviceThatUsesMockedService = serviceThatUsesMockedService;
}
@Bean
public ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService() {
return new ServiceThatUsesTheServiceThatUsesMockedService(this.serviceThatUsesMockedService);
}
}
@Configuration
public static class AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration {
private final ServiceThatUsesMockedService serviceThatUsesMockedService;
private final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService;
public AnotherServiceThatUsesTheServiceThatUsesMockedServiceAutoConfiguration(
final ServiceThatUsesMockedService serviceThatUsesMockedService,
final ServiceThatUsesTheServiceThatUsesMockedService serviceThatUsesTheServiceThatUsesMockedService) {
this.serviceThatUsesMockedService = serviceThatUsesMockedService;
this.serviceThatUsesTheServiceThatUsesMockedService = serviceThatUsesTheServiceThatUsesMockedService;
}
@Bean
public AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService() {
return new AnotherServiceThatUsesTheServiceThatUsesMockedService(this.serviceThatUsesMockedService,
this.serviceThatUsesTheServiceThatUsesMockedService);
}
}
}
package org.springframework.test.context.bean.override;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.context.TestConstructor.AutowireMode.*;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestConstructor;
import org.springframework.test.context.bean.override.BeanOverrideHandlerBug.AnotherServiceThatUsesTheServiceThatUsesMockedService;
import org.springframework.test.context.bean.override.BeanOverrideHandlerBug.WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceBad;
import org.springframework.test.context.bean.override.BeanOverrideHandlerBug.WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceGood;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SuppressWarnings("javadoc")
public class BeanOverrideHandlerBugTest {
static void assertService(
final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService) {
assertNotNull(anotherServiceThatUsesTheServiceThatUsesMockedService);
assertNotNull(anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesMockedService());
assertNotNull(anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesMockedService()
.getMockedService());
assertNotNull(
anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesTheServiceThatUsesMockedService());
assertNotNull(
anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesTheServiceThatUsesMockedService()
.getServiceThatUsesMockedService());
assertNotNull(
anotherServiceThatUsesTheServiceThatUsesMockedService.getServiceThatUsesTheServiceThatUsesMockedService()
.getServiceThatUsesMockedService()
.getMockedService());
}
@SpringJUnitConfig
@TestConstructor(autowireMode = ALL)
@WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceBad
static class BeanOverrideHandlerBugTestFails {
private final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService;
public BeanOverrideHandlerBugTestFail(
final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService) {
this.anotherServiceThatUsesTheServiceThatUsesMockedService
= anotherServiceThatUsesTheServiceThatUsesMockedService;
}
/*
* Fails with java.lang.IllegalStateException: Duplicate BeanOverrideHandler discovered in test class
* org.springframework.test.context.bean.override.BeanOverrideHandlerBugTest$BeanOverrideHandlerBugTestFail:
* [MockitoBeanOverrideHandler@4650a407 field = [null], beanType =
* org.springframework.test.context.bean.override.BeanOverrideHandlerBug$MockedService, beanName = [null],
* contextName = '', strategy = REPLACE_OR_CREATE, reset = AFTER, extraInterfaces = set[[empty]], answers =
* RETURNS_DEFAULTS, serializable = false]
*
*/
@Test
void test() {
assertService(this.anotherServiceThatUsesTheServiceThatUsesMockedService);
}
}
@SpringJUnitConfig
@TestConstructor(autowireMode = ALL)
@WithAnotherServiceThatUsesTheServiceThatUsesMockedServiceGood
static class BeanOverrideHandlerBugTestPasses {
private final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService;
public BeanOverrideHandlerBugTestPass(
final AnotherServiceThatUsesTheServiceThatUsesMockedService anotherServiceThatUsesTheServiceThatUsesMockedService) {
this.anotherServiceThatUsesTheServiceThatUsesMockedService
= anotherServiceThatUsesTheServiceThatUsesMockedService;
}
@Test
void test() {
assertService(this.anotherServiceThatUsesTheServiceThatUsesMockedService);
}
}
}
The error:
java.lang.IllegalStateException: Duplicate BeanOverrideHandler discovered in test class org.springframework.test.context.bean.override.BeanOverrideHandlerBugTest$BeanOverrideHandlerBugTestFail: [MockitoBeanOverrideHandler@4650a407 field = [null], beanType = org.springframework.test.context.bean.override.BeanOverrideHandlerBug$MockedService, beanName = [null], contextName = '', strategy = REPLACE_OR_CREATE, reset = AFTER, extraInterfaces = set[[empty]], answers = RETURNS_DEFAULTS, serializable = false]
at org.springframework.util.Assert.state(Assert.java:101)
at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.lambda$findBeanOverrideHandlers$2(BeanOverrideContextCustomizerFactory.java:61)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.findBeanOverrideHandlers(BeanOverrideContextCustomizerFactory.java:61)
at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.createContextCustomizer(BeanOverrideContextCustomizerFactory.java:49)
at org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactory.createContextCustomizer(BeanOverrideContextCustomizerFactory.java:38)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.getContextCustomizers(AbstractTestContextBootstrapper.java:360)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:332)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedContextConfiguration(AbstractTestContextBootstrapper.java:244)
at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildTestContext(AbstractTestContextBootstrapper.java:108)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:142)
at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:126)
at org.springframework.test.context.junit.jupiter.SpringExtension.getTestContextManager(SpringExtension.java:362)
at org.springframework.test.context.junit.jupiter.SpringExtension.beforeAll(SpringExtension.java:128)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Spring Framework 6.2.9 Spring Boot 3.5.3