I am developing a Spring Boot application. For my regular service class unit tests, I am able to extend my test class with MockitoExtension
, and the mocks are strict, which is what I want.
interface MyDependency {
Integer execute(String param);
}
class MyService {
@Autowired MyDependency myDependency;
Integer execute(String param) {
return myDependency.execute(param);
}
}
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
@Mock
MyDependency myDependency;
@InjectMocks
MyService myService;
@Test
void execute() {
given(myDependency.execute("arg0")).willReturn(4);
myService.execute("arg1"); //will throw exception
}
}
In this case, the an exception gets thrown with the following message (redacted):
org.mockito.exceptions.misusing.PotentialStubbingProblem:
Strict stubbing argument mismatch. Please check:
- this invocation of 'execute' method:
myDependency.execute(arg1);
- has following stubbing(s) with different arguments:
1. myDependency.execute(arg0);
In addition, if the stubbing was never used there would be the following (redacted):
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at MyServiceTest.execute()
However, when I use @MockBean
in an integration test, then none of the strict behavior is present. Instead, the stubbed method returns null because the stubbing "fails" silently. This is behavior that I do not want. It is much better to fail immediately when unexpected arguments are used.
@SpringBootTest
class MyServiceTest {
@MockBean
MyDependency myDependency;
@Autowired
MyService myService;
@Test
void execute() {
given(myDependency.execute("arg0")).willReturn(4);
myService.execute("arg1"); //will return null
}
}
Is there any workaround for this?
As mentioned in this comment, this GitHub issue in the spring-boot project addresses this same problem and has remained open since 2019, so it's unlikely that an option for "strict stubs" will be available in @SpringBootTest
classes anytime soon.
One way that Mockito recommends to enable "strict stubs" is to start a MockitoSession
with Strictness.STRICT_STUBS
before each test, and close the MockitoSession
after each test. Mockito mocks for @MockBean
properties in @SpringBootTest
classes are generated by Spring Boot's MockitoPostProcessor
, so a workaround would need to create the MockitoSession
before the MockitoPostProcessor
runs. A custom TestExecutionListener
can be implemented to handle this, but only its beforeTestClass
method would run before the MockitoPostProcessor
. The following is such an implementation:
public class MyMockitoTestExecutionListener implements TestExecutionListener, Ordered {
// Only one MockitoSession can be active per thread, so ensure that multiple instances of this listener on the
// same thread use the same instance
private static ThreadLocal<MockitoSession> mockitoSession = ThreadLocal.withInitial(() -> null);
// Count the "depth" of processing test classes. A parent class is not done processing until all @Nested inner
// classes are done processing, so all @Nested inner classes must share the same MockitoSession as the parent class
private static ThreadLocal<Integer> depth = ThreadLocal.withInitial( () -> 0 );
@Override
public void beforeTestClass(TestContext testContext) {
depth.set(depth.get() + 1);
if (depth.get() > 1)
return; // @Nested classes share the MockitoSession of the parent class
mockitoSession.set(
Mockito.mockitoSession()
.strictness(Strictness.STRICT_STUBS)
.startMocking()
);
}
@Override
public void afterTestClass(TestContext testContext) {
depth.set(depth.get() - 1);
if (depth.get() > 0)
return; // @Nested classes should let the parent class end the MockitoSession
MockitoSession session = mockitoSession.get();
if (session != null)
session.finishMocking();
mockitoSession.remove();
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
Then, MyMockitoTestExecutionListener
can be added as a listener in test classes:
@SpringBootTest
@TestExecutionListeners(
listeners = {MyMockitoTestExecutionListener.class},
mergeMode = MergeMode.MERGE_WITH_DEFAULTS
)
public class MySpringBootTests {
@MockBean
Foo mockFoo;
// Tests using mockFoo...
}
Alternatively, it can be enabled globally by putting the following in src/test/resources/META-INF/spring.factories
:
org.springframework.test.context.TestExecutionListener=\
com.my.MyMockitoTestExecutionListener