Search code examples
javaspring-bootmockitospring-boot-test

Strict @MockBean in a Spring Boot Test


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?


Solution

  • 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