Spring Boot 测试中的严格@MockBean

Jos*_*uss 10 java mockito spring-boot spring-boot-test

我正在开发一个 Spring Boot 应用程序。对于我的常规服务类单元测试,我可以使用 扩展我的测试类MockitoExtension,并且模拟很严格,这就是我想要的

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
  }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,将引发异常并显示以下消息(已编辑):

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);
Run Code Online (Sandbox Code Playgroud)

此外,如果从未使用过存根,则会出现以下内容(已编辑):

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()
Run Code Online (Sandbox Code Playgroud)

但是,当我@MockBean在集成测试中使用时,不存在任何严格的行为。相反,存根方法会返回 null,因为存根会默默地“失败”。这是我不想要的行为。当使用意外的参数时,最好立即失败。

@SpringBootTest
class MyServiceTest {
  @MockBean
  MyDependency myDependency;

  @Autowired
  MyService myService;

  @Test
  void execute() {
    given(myDependency.execute("arg0")).willReturn(4);
    
    myService.execute("arg1"); //will return null
  }
}
Run Code Online (Sandbox Code Playgroud)

有什么解决方法吗?

Gon*_*n I 1

是的,有一些解决方法,但它非常复杂。最好等待 Mockito 4,其中默认为严格模​​拟。

第一个选项:

  1. 将 @MockBean 替换为 @Autowired,并使用 @Primary 进行测试配置(这应该具有与 @MockBean 相同的效果,将其插入到应用程序以及测试中)

  2. 创建一个默认答案,为任何未存根的函数抛出异常

然后用一些存根覆盖这个答案 - 但你必须使用 doReturn 而不是 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
    }

}
Run Code Online (Sandbox Code Playgroud)

另一种可能但远非理想的选择:不使用默认答案,而是首先使用 any() 匹配器对所有函数调用进行存根,然后使用您实际期望的值。

这会起作用,因为存根顺序很重要,最后一场比赛获胜。

但同样:您将必须使用 doXXX() 系列存根调用,更糟糕的是,您将必须存根每个可能的函数以接近严格的模拟。

// 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
    }

}
Run Code Online (Sandbox Code Playgroud)

另一种选择是等到使用模拟测试完成后,然后使用

verifyNoMoreInteractions();
Run Code Online (Sandbox Code Playgroud)