简体   繁体   中英

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?

It is due to the strictness applied by Mockito.

org.mockito.exceptions.misusing.PotentialStubbingProblem: 
Strict stubbing argument mismatch. Please check:

The first exception is due to the fact that your arguments are differents. "arg0" <> "arg1" .

org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.

The message is clear enought and generaly occurs when you create a mock and don't use it.

To resolve it, you can set the level ofstrictness to LENIENT in Mockito.

In the case of @MockBean , I guess strictness is not manage while testing. More details about that will be welcome.

Yes there are some workarounds but it is quite involved. It may be better to just wait for Mockito 4 where the default will be strict mocks.

The first option:

  1. Replace @MockBean with @Autowired with a test configuration with @Primary ( this should give the same effect as @MockBean, inserting it into the application as well as into the test )

  2. Create a default answer that throws an exception for any unstubbed function

Then override that answer with some stubbing - but you have to use doReturn instead of when thenReturn

// this is the component to mock
@Component
class ExtService {
    int f1(String a) {
        return 777;
    }
}
// this is the test class
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationTests {

    static class RuntimeExceptionAnswer implements Answer<Object> {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            throw new RuntimeException(
                    invocation.getMethod().getName() + " was not stubbed with the received arguments");
        }
    }

    @TestConfiguration
    public static class TestConfig {

        @Bean
        @Primary
        public ExtService mockExtService() {
            ExtService std = Mockito.mock(ExtService.class, new RuntimeExceptionAnswer());
            return std;
        }

    }

    // @MockBean ExtService extService;
    @Autowired
    ExtService extService; // replace mockBean

    @Test
    public void contextLoads() {

        Mockito.doReturn(1).when(extService).f1("abc"); // stubbing has to be in this format
        System.out.println(extService.f1("abc")); // returns 1
        System.out.println(extService.f1("abcd")); // throws exception
    }

}

Another possible but far from ideal option: instead of using a default answer is to stub all your function calls first with an any() matcher, then later with the values you actually expect.

This will work because the stubbing order matters, and the last match wins.

But again: you will have to use the doXXX() family of stubbing calls, and worse you will have to stub every possible function to come close to a strict mock.

// this is the service we want to test
@Component
class ExtService {
    int f1(String a) {
        return 777;
    }
}
// this is the test class
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationTests {


    @MockBean ExtService extService;

    @Test
    public void contextLoads() {

        Mockito.doThrow(new RuntimeException("unstubbed call")).when(extService).f1(Mockito.any()); // stubbing has to be in this format
        Mockito.doReturn(1).when(extService).f1("abc"); // stubbing has to be in this format
        System.out.println(extService.f1("abc")); // returns 1
        System.out.println(extService.f1("abcd")); // throws exception
    }

}

Yet another option is to wait until after the test finishes using the mock, and then use

verifyNoMoreInteractins();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM