spring-projects / spring-projects/spring-framework
BeanOverrideHandler fails when a single mock is declared via multiple meta-annotation paths
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 60.2k
- Forks
- 38.8k
- Avg merge
- 5d 2h
- Merged PRs (30d)
- 27
Description
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at BeanOverrideContextCustomizerFactory.findBeanOverrideHandlers and use the BeanOverrideHandlerBugTest reproduction to trace how the same MockitoBean is discovered through multiple meta-annotation paths. Done means the duplicate handler exception no longer occurs and the supplied assertions pass while the single mock is shared by the dependent services.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100