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!