美文网首页Java
【实战技巧】使用(或不使用)Spring Boot进行模拟

【实战技巧】使用(或不使用)Spring Boot进行模拟

作者: Java余笙 | 来源:发表于2020-04-02 22:18 被阅读0次

前言:

Mockito是一个非常流行的库,可以支持测试。它允许我们用“模拟”代替真实的对象,即用非真实的对象以及可以在测试中控制其行为的对象。

本文简要介绍了Mockito和Spring Boot与之集成的方式和原因。

被测系统
本文测试的系统将是Spring REST控制器,该控制器接受将资金从一个帐户转移到另一个帐户的请求:

@RestController
@RequiredArgsConstructor
public class SendMoneyController {

  private final SendMoneyUseCase sendMoneyUseCase;

  @PostMapping(path = "/sendMoney/{sourceAccountId}/{targetAccountId}/{amount}")
  ResponseEntity sendMoney(
          @PathVariable("sourceAccountId") Long sourceAccountId,
          @PathVariable("targetAccountId") Long targetAccountId,
          @PathVariable("amount") Integer amount) {
  
    SendMoneyCommand command = new SendMoneyCommand(
            sourceAccountId,
            targetAccountId,
            amount);
  
    boolean success = sendMoneyUseCase.sendMoney(command);
    
    if (success) {
      return ResponseEntity
              .ok()
              .build();
    } else {
      return ResponseEntity
              .status(HttpStatus.INTERNAL_SERVER_ERROR)
              .build();
    }
  }

}

控制器将输入传递给具有SendMoneyUseCase单个方法的接口实例:

public interface SendMoneyUseCase {

  boolean sendMoney(SendMoneyCommand command);

  @Value
  @Getter
  @EqualsAndHashCode(callSuper = false)
  class SendMoneyCommand {

    private final Long sourceAccountId;
    private final Long targetAccountId;
    private final Integer money;

    public SendMoneyCommand(
            Long sourceAccountId,
            Long targetAccountId,
            Integer money) {
      this.sourceAccountId = sourceAccountId;
      this.targetAccountId = targetAccountId;
      this.money = money;
    }
  }

}

最后,我们有一个实现SendMoneyUseCase接口的虚拟服务:

@Slf4j
@Component
public class SendMoneyService implements SendMoneyUseCase {

  public SendMoneyService() {
    log.info(">>> constructing SendMoneyService! <<<");
  }

  @Override
  public boolean sendMoney(SendMoneyCommand command) {
    log.info("sending money!");
    return false;
  }

}

想象一下,该类中有一些非常复杂的业务逻辑正在代替日志记录语句。

对于本文的大部分内容,我们对SendMoneyUseCase接口的实际实现不感兴趣。毕竟,我们想在对Web控制器的测试中模拟它。

为什么要模拟?

为什么在测试中我们应该使用模拟而不是真实的服务对象?

想象一下,上面的服务实现依赖于数据库或某些其他第三方系统。我们不想针对数据库运行测试,如果数据库不可用,即使我们的被测系统可能完全没有错误,测试也将失败。我们在测试中添加的依赖项越多,测试失败的原因就越多。如果我们改用模拟,则可以模拟掉所有潜在的故障。

除了减少故障之外,模拟还降低了测试的复杂性,从而节省了我们的精力。设置用于测试的正确初始化的对象的整个网络需要大量样板代码。使用模拟,我们只需要“实例化”一个模拟即可,而不是整个对象的真实实例可能需要实例化。

总之,我们希望从可能复杂,缓慢且不稳定的集成测试过渡到简单,快速和可靠的单元测试。

因此,在上述测试中,我们要使用具有相同接口的模拟程序SendMoneyController,而不是的真实实例SendMoneyUseCase,该接口的行为我们可以在测试中根据需要进行控制。

使用Mockito进行模拟(并且没有Spring)

作为模拟框架,我们将使用Mockito,因为它是全面、完善和集成到Spring Boot中的。

但是最好的测试根本不使用Spring,因此让我们首先来看一下如何在普通的单元测试中使用Mockito来消除不需要的依赖项。

普通Mockito测试

使用Mockito的最简单的方法是使用实​​例化一个模拟对象Mockito.mock(),然后将如此创建的模拟对象传递给被测类:

public class SendMoneyControllerPlainTest {

  private SendMoneyUseCase sendMoneyUseCase = 
      Mockito.mock(SendMoneyUseCase.class);

  private SendMoneyController sendMoneyController = 
      new SendMoneyController(sendMoneyUseCase);

  @Test
  void testSuccess() {
    // given
    SendMoneyCommand command = new SendMoneyCommand(1L, 2L, 500);
    given(sendMoneyUseCase
        .sendMoney(eq(command)))
        .willReturn(true);
  
    // when
    ResponseEntity response = sendMoneyController
        .sendMoney(1L, 2L, 500);
  
    // then
    then(sendMoneyUseCase)
        .should()
        .sendMoney(eq(command));
  
    assertThat(response.getStatusCode())
        .isEqualTo(HttpStatus.OK);
  }

}

我们创建的模拟实例,SendMoneyService并将该模拟传递给的构造函数SendMoneyController。控制器不知道它是模拟的,会像对待真实物体一样对待它。

在测试本身中,我们可以使用Mockito given()来定义我们希望该模拟具有的行为,并then()检查是否已按预期调用了某些方法,可以在docs中找到有关Mockito的模拟和验证方法的更多信息。

Web控制器应该经过集成测试!

不要在家做!上面的代码只是如何创建模拟的示例。使用这样的单元测试来测试Spring Web Controller只能覆盖生产中可能发生的潜在错误的一小部分。上面的单元测试验证是否返回了特定的响应代码,但是它没有与Spring集成以检查是否从HTTP请求中正确解析了输入参数,或者控制器是否侦听了正确的路径,或者是否将异常转换为预期的HTTP响应,依此类推。

如我在有关@WebMvcTest注解的文章中所讨论的,Web控制器应该与Spring集成进行测试。

在JUnit Jupiter中使用Mockito注释

Mockito提供了一些方便的注释,减少了创建模拟实例并将它们传递到我们要测试的对象中的手动工作。

使用JUnit Jupiter,我们需要将应用于MockitoExtension测试:

@ExtendWith(MockitoExtension.class)
class SendMoneyControllerMockitoAnnotationsJUnitJupiterTest {

  @Mock
  private SendMoneyUseCase sendMoneyUseCase;

  @InjectMocks
  private SendMoneyController sendMoneyController;

  @Test
  void testSuccess() {
    ...
  }

}

然后,我们可以在测试字段中使用@Mock和@InjectMocks注释。

带注解的字段@Mock将自动使用其类型的模拟实例进行初始化,就像我们Mockito.mock()手动调用一样。

然后,Mockito将尝试@InjectMocks通过将所有模拟传递到构造函数来实例化带注释的字段。请注意,我们需要为Mockito提供这样的构造函数以使其可靠地工作。如果Mockito找不到构造函数,它将尝试使用setter注入或字段注入,但是最干净的方法仍然是构造函数。您可以在Mockito的Javadoc中阅读有关其背后的算法的信息。

在JUnit 4中使用Mockito注释

与JUnit 4相似,除了我们需要使用MockitoJUnitRunner代替MockitoExtension:

@RunWith(MockitoJUnitRunner.class)
public class SendMoneyControllerMockitoAnnotationsJUnit4Test {

  @Mock
  private SendMoneyUseCase sendMoneyUseCase;

  @InjectMocks
  private SendMoneyController sendMoneyController;

  @Test
  public void testSuccess() {
    ...
  }

}

使用Mockito和Spring Boot进行模拟

有时候,我们不得不依靠Spring Boot为我们设置应用程序上下文,因为手动实例化整个类的网络将耗费大量精力。

但是,我们可能不想在某个测试中测试所有Bean之间的集成,因此,我们需要一种在模拟的Spring应用程序上下文中替换某些Bean的方法。Spring Boot 为此提供了@MockBean和@SpyBean注释。

用@MockBean添加一个模拟Spring Bean

一个使用模拟的主要示例是使用Spring Boot's @WebMvcTest创建一个应用程序上下文,其中包含测试Spring Web控制器所需的所有bean:

@WebMvcTest(controllers = SendMoneyController.class)
class SendMoneyControllerWebMvcMockBeanTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private SendMoneyUseCase sendMoneyUseCase;

  @Test
  void testSendMoney() {
    ...
  }

}

即使由Bean 创建的应用程序上下文被标记为带有注释的Spring Bean,@WebMvcTest也不会选择我们的SendMoneyServiceBean(实现SendMoneyUseCase接口)@Component。我们必须自己提供一个类型的bean SendMoneyUseCase,否则,将得到如下错误:

No qualifying bean of type 'io.reflectoring.mocking.SendMoneyUseCase' available:
  expected at least 1 bean which qualifies as autowire candidate.

无需实例化SendMoneyService自己或告诉Spring进行拾取,而可能在过程中牵扯到其他bean,我们可以SendMoneyUseCase向应用程序上下文中添加一个模拟实现。

通过使用Spring Boot的@MockBean注释可以轻松完成此操作。然后,Spring Boot测试支持将自动创建Mockito模拟类型SendMoneyUseCase,并将其添加到应用程序上下文中,以便我们的控制器可以使用它。在测试方法中,我们可以像上面一样使用Mockito given()和when()方法。

这样,我们可以轻松地创建一个集中化的Web控制器测试,该测试仅实例化所需的对象。

用@MockBean替换Spring Bean

除了添加新的(模拟)bean之外,我们可以@MockBean类似地使用模拟来替换应用程序上下文中已经存在的bean:

@SpringBootTest
@AutoConfigureMockMvc
class SendMoneyControllerSpringBootMockBeanTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private SendMoneyUseCase sendMoneyUseCase;

  @Test
  void testSendMoney() {
    ...
  }

}

请注意,上面的测试使用@SpringBootTest代替@WebMvcTest,这意味着将为此测试创建Spring Boot应用程序的完整应用程序上下文。这包括我们的SendMoneyServicebean,因为它带有注释@Component并位于我们的应用程序类的包结构之内。

该@MockBean注释将导致春季寻找类型的现有的bean SendMoneyUseCase在应用程序上下文。如果存在,它将用Mockito模拟替换该bean。

最终结果是相同的:在我们的测试中,我们可以sendMoneyUseCase像对待Mockito模拟一样对待对象。

区别在于,在SendMoneyService创建初始应用程序上下文之前,将在使用模拟替换Bean之前实例化Bean。如果SendMoneyService在其构造函数中执行了某些操作,而该操作需要依赖于测试时不可用的数据库或第三方系统,则此操作将无效。除了使用之外@SpringBootTest,我们还必须创建一个更具针对性的应用程序上下文,并在实例化实际的bean之前将模拟添加到应用程序上下文中。

使用@SpyBean监视Spring Bean

Mockito还允许我们监视真实对象。Mockito不会完全嘲笑一个对象,而是围绕真实对象创建一个代理,并仅监视正在调用的方法,以便稍后我们可以验证是否已调用某个方法。

Spring Boot @SpyBean为此提供了注释:

@SpringBootTest
@AutoConfigureMockMvc
class SendMoneyControllerSpringBootSpyBeanTest {

  @Autowired
  private MockMvc mockMvc;

  @SpyBean
  private SendMoneyUseCase sendMoneyUseCase;

  @Test
  void testSendMoney() {
    ...
  }

}

@SpyBean就像@MockBean。与其在应用程序上下文中添加或替换bean,不如将其包装在Mockito的代理中。在测试中,我们可以then()如上所述使用Mockito 验证方法调用。

为什么我的Spring测试需要这么长时间?

如果我们使用@MockBean,并@SpyBean在我们的测试了很多,运行测试将花费大量的时间。这是因为Spring Boot为每个测试创建了一个新的应用程序上下文,根据应用程序上下文的大小,这可能是一项昂贵的操作。

结论

Mockito使我们可以轻松地模拟掉我们现在不想测试的对象。这可以减少我们测试中的集成开销,甚至可以将集成测试转变为更具针对性的单元测试。

Spring Boot通过使用@MockBean和@SpyBean注释,可以轻松地在Spring支持的集成测试中使用Mockito的模拟功能。

尽管这些Spring Boot功能要包含在我们的测试中一样容易,但我们应该意识到成本:每个测试都可能创建一个新的应用程序上下文,从而潜在地增加了我们测试套件的运行时间。

相关文章

网友评论

    本文标题:【实战技巧】使用(或不使用)Spring Boot进行模拟

    本文链接:https://www.haomeiwen.com/subject/lmxqphtx.html