Using Spring Boot 3.4.2

Bug Reports Given the following classes

@Component
public class BaseService {

    public void doSomething() {}
}
@Primary
@Component
public class ServiceB extends BaseService {
}
@Component
public class ServiceA {

    private final BaseService serviceB;

    public ServiceA(BaseService serviceB) {
        this.serviceB = serviceB;
    }

    public void callB() {
        serviceB.doSomething();
    }
}

The following test works with @MockBean but fails with @MockitoBean because the mocked bean is not injected into ServiceA

@SpringBootTest
class MockitobeanApplicationTests {

    @Autowired
    ServiceA serviceA;

    @MockitoBean
    BaseService baseService;

    @Test
    void contextLoads() {
        serviceA.callB();

        verify(baseService).doSomething();
    }

}

Enhancements requests This used to/does work with @MockBean

Comment From: sbrannen

Hi @patdlanger,

Congratulations on submitting your first issue for the Spring Framework! 👍

I tried to reproduce the behavior you're reporting with the following.

@SpringJUnitConfig
public class MockPrimaryBeanTests {

    @Autowired
    ServiceA serviceA;

    @MockitoBean
    BaseService baseService;


    @Test
    void contextLoads() {
        serviceA.callB();

        verify(baseService).doSomething();
    }


    @Configuration
    @Import({ BaseService.class, ServiceA.class, ServiceB.class })
    static class Config {
    }

    @Component
    public static class BaseService {

        public void doSomething() {
        }
    }

    @Primary
    @Component
    public static class ServiceB extends BaseService {
    }

    @Component
    public static class ServiceA {

        private final BaseService serviceB;


        public ServiceA(BaseService serviceB) {
            this.serviceB = serviceB;
        }

        public void callB() {
            this.serviceB.doSomething();
        }
    }

}

But that passes for me with @MockBean and @MockitoBean (against the 6.2.x branch).

With which version of spring-test did you encounter the failure?

Comment From: sbrannen

Related Issues

  • 33819

Comment From: patdlanger

Hi @sbrannen , you are right. With your test setup both @MockitoBean and @MockBean work. It seems that the issue lies in @SpringBootTest . I pushed a small repo here https://github.com/patdlanger/mockitobeanspring/blob/main/src/test/java/com/mockitobean/MockitobeanSpringBootTest.java The test which uses @SpringBootTest fails

Comment From: sbrannen

Thanks for the feedback and the reproducer, @patdlanger. 👍

It seems that the issue lies in @SpringBootTest.

On the surface, yes, that appears to be the case.

However, after debugging it, I figured out that the core difference results from the use of @ComponentScan for @SpringBootTest vs. the use of @Import in the example I created.

With component scanning the bean names are serviceA, serviceB, and baseService. Whereas, with @Import the bean names are the fully-qualified class names for the components.

With @MockBean, the field name is not used as a fallback qualifier, and @Primary is always honored. However, with @MockitoBean, the field name is used as a fallback qualifier which is applied before honoring @Primary.

The latter is what causes this bug.

Specifically, when the name of the BaseService bean is baseService, @MockitoBean ends up mocking the BaseService bean instead of the ServiceB bean, even though the ServiceB bean is the @Primary bean of type BaseService.

In the following modified version of my example, each @Component declares an explicit bean name which matches the bean name generated during component scanning.

The test class fails "as is", but if you change the name of the field to something other than baseService (for example, baseServiceX) the test then passes. Again, the reason is that the name of the field will no longer match eagerly as the fallback qualifier before applying @Primary semantics.

@SpringJUnitConfig
class MockPrimaryBeanTests {

    @Autowired
    ServiceA serviceA;

    @MockitoBean
    BaseService baseService;

    @Test
    void contextLoads() {
        serviceA.callB();

        verify(baseService).doSomething();
    }

    @Configuration
    @Import({ BaseService.class, ServiceA.class, ServiceB.class })
    static class Config {
    }

    @Component("baseService")
    static class BaseService {
        public void doSomething() {
        }
    }

    @Primary
    @Component("serviceB")
    static class ServiceB extends BaseService {
    }

    @Component("serviceA")
    static class ServiceA {

        private final BaseService serviceB;

        public ServiceA(BaseService serviceB) {
            this.serviceB = serviceB;
        }

        public void callB() {
            this.serviceB.doSomething();
        }
    }
}

Comment From: sbrannen

I just pushed a fix for this which will be available in the upcoming 6.2.3 release.

However, @patdlanger, it would be great if you could try out a 6.2.3 snapshot before the release to confirm that everything is good on your end.

Cheers!