Skip to content

BeanOverrideHandler fails when a single mock is declared via multiple meta-annotation paths #35231

@jmax01

Description

@jmax01

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions