无论您是遵循传统的测试金字塔还是采用诸如 “测试蜂窝” 这样的较新方法,都应该在开发过程中的某个时候开始编写集成测试用例。
您可以编写不同类型的集成测试。从持久性测试开始,您可以检查组件之间的交互,也可以模拟调用外部服务。本文将讨论后一种情况。
在谈论 WireMock 之前,让我们从一个典型的例子开始。

ChuckNorrisService

我们有一个简单的 API,用于手动测试。在 “业务” 类意外是,它可以调用外部 API。它使用 Spring 框架提供功能的。没什么特别的。我多次看到的是模拟 RestTemplate 并返回一些预先确定的答案的测试。该实现可能如下所示:

@Service
public class ChuckNorrisService{
...
  public ChuckNorrisFact retrieveFact() {
    ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
    return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
  }
 ...
 }

在检查成功案例的常规单元测试旁边,将至少有一项覆盖 HTTP 错误码的测试用例,即 4xx 或 5xx 状态代码:

@Test
  public void shouldReturnBackupFactInCaseOfError() {
    String url = "http://localhost:8080";
    RestTemplate mockTemplate = mock(RestTemplate.class);
    ResponseEntity<ChuckNorrisFactResponse> responseEntity = new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
    when(mockTemplate.getForEntity(url, ChuckNorrisFactResponse.class)).thenReturn(responseEntity);
    ChuckNorrisService service = new ChuckNorrisService(mockTemplate, url);
    ChuckNorrisFact retrieved = service.retrieveFact();
    assertThat(retrieved).isEqualTo(ChuckNorrisService.BACKUP_FACT);
  }

看起来还不错吧?响应实体返回 503 错误代码,我们的服务不会崩溃。所有测试都是绿色通过的,我们可以部署我们的应用程序。
不幸的是,Spring 的 RestTemplate 不能这样使用。方法签名 getForEntity 给了我们很小的提示。它指出 throws RestClientException。这就是 mock 的 RestTemplate 与实际实现不同的地方。我们将永远不会收到 ResponseEntity 带有 4xx 或 5xx 状态代码的。RestTemplate 将抛出的子类 RestClientException。通过查看类的层次结构,我们可以对可能抛出的结果有一个很好的印象:

因此,让我们看看如何使这项测试更好。

WireMock 进行拯救

WireMock 通过启动模拟服务器并返回将其配置为返回的答案来模拟 Web 服务。得益于出色的 DSL,它很容易集成到您的测试中,并且模拟请求也很简单。

对于 JUnit 4,有一个 WireMockRule 有助于启动停止服务器的工具。对于 JUnit 5,大概需要自己做一个这样的工具。当您检查示例项目时,您可以找到 ChuckNorrisServiceIntegrationTest。这是基于 JUnit 4 的 SpringBoot 测试。让我们看一下。

最重要的部分是 ClassRule:

@ClassRule
  public static WireMockRule wireMockRule = new WireMockRule();

如前所述,这将启动和停止 WireMock 服务器。您也可以像往常一样使用该规则 Rule 来启动和停止每个测试的服务器。对于我们的测试,这不是必需的。

接下来,您将看到几种 configureWireMockFor...方法。这些包含 WireMock 何时返回答案的说明。将 WireMock 配置分为几种方法并从测试中调用它们是我使用 WireMock 的方法。当然,您可以在一个@Before方法中设置所有可能的请求。对于正确使用的 Demo,我们这样做:

public void configureWireMockForOkResponse(ChuckNorrisFact fact) throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", fact);
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))));
  }

所有方法都是从静态导入的com.github.tomakehurst.wiremock.client.WireMock。如您所见,我们将 HTTP GET 存入路径/jokes/random并返回 JSON 对象。该okJson()方法只是带有 JSON 内容的 200 响应的简写。对于错误情况,代码甚至更简单:

private void configureWireMockForErrorResponse() {
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(serverError()));
  }

如您所见,DSL 使阅读说明变得容易。将 WireMock 放置在适当的位置,我们可以看到我们先前的实现不起作用,因为 RestTemplate 引发了异常。因此,我们必须调整代码:

public ChuckNorrisFact retrieveFact() {
    try {
      ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
      return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
    } catch (HttpStatusCodeException e){
      return BACKUP_FACT;
    }
  }

这已经涵盖了 WireMock 的基本用例。配置请求的答案,执行测试,检查结果,so easy,就这么简单。尽管如此,在云环境中运行测试时通常会遇到一个问题。让我们看看我们能做什么。

动态端口上的 WireMock

您可能已经注意到,项目中的集成测试包含一个
ApplicationContextInitializer类,并且其@TestPropertySource注释会覆盖实际 API 的 URL。那是因为我想在随机端口上启动 WireMock。当然,您可以为 WireMock 配置一个固定端口,并在测试中将此端口用作常量来处理。但是,如果您的测试在某些云提供商的基础架构上运行,则无法确定该端口是否可用。因此,我认为随机端口更好。

不过,在 Spring 应用程序中使用属性时,我们必须以某种方式将随机端口传递给我们的服务。或者,如您在示例中看到的那样,覆盖 URL。这就是为什么我们使用ApplicationContextInitializer。我们将动态分配的端口添加到应用程序上下文中,然后可以使用属性来引用它${wiremock.port}。这里唯一的缺点是我们现在必须使用ClassRule。否则,我们无法在初始化 Spring 应用程序之前访问端口。

解决了此问题后,让我们看一下涉及 HTTP 调用的一个常见问题。

超时时间

WireMock 提供了更多的响应可能性,而不仅仅是对 GET 请求的简单答复。经常被遗忘的另一个测试案例是测试超时。开发人员往往会忘记在RestTemplate设置超时URLConnections。如果没有超时,则两者都将等待无限量的时间来进行响应。在最好的情况下,在最坏的情况下,所有线程都将等待永远不会到达的响应。

因此,我们应该添加一个模拟超时的测试。当然,我们也可以使用 Mockito 模拟来创建延迟,但是在这种情况下,我们将再次猜测 RestTemplate 的行为。使用 WireMock 模拟延迟非常简单:

private void configureWireMockForSlowResponse() throws JsonProcessingException {
    ChuckNorrisFactResponse chuckNorrisFactResponse = new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, ""));
    stubFor(get(urlEqualTo("/jokes/random"))
        .willReturn(
            okJson(OBJECT_MAPPER.writeValueAsString(chuckNorrisFactResponse))
                .withFixedDelay((int) Duration.ofSeconds(10L).toMillis())));
  }

withFixedDelay()期望一个表示毫秒的 int 值。我更喜欢使用Duration或至少一个表示该参数表示毫秒的常量,而不必每次写代码都需要看一下代码注释。

设置超时RestTemplate并添加响应的测试后,我们可以看到RestTemplate抛出ResourceAccessException。因此,我们可以调整 catch 块以捕获此异常和,HttpStatusCodeException或者仅捕获两者的超类:

public ChuckNorrisFact retrieveFact() {
    try {
      ResponseEntity<ChuckNorrisFactResponse> response = restTemplate.getForEntity(url, ChuckNorrisFactResponse.class);
      return Optional.ofNullable(response.getBody()).map(ChuckNorrisFactResponse::getFact).orElse(BACKUP_FACT);
    } catch (RestClientException e){
      return BACKUP_FACT;
    }
  }

现在,我们已经很好地介绍了执行 HTTP 请求时最常见的情况,并且可以确定我们正在测试接近真实条件的条件。

为什么不?

HTTP 集成测试的另一个选择是Hoverfly。它的工作原理类似于WireMock,但我更喜欢后者。原因是在运行包含浏览器的端到端测试时,WireMock 也非常有用。Hoverfly(至少是 Java 库)受 JVM 代理的限制。这可能使它比WireMock更快,但是当例如某些JavaScript代码开始起作用时,它根本不起作用。当您的浏览器代码也直接调用其他一些服务时,WireMock 启动 Web 服务器这一功能非常有用。然后,您也可以使用 WireMock 来 mock 它们,并编写例如 Selenium 测试。

结论

本文可以向您展示两件事:

当然,这两个主题都可以写出非常多的文章。尽管如此,还是分享了如何使用 WireMock 及其功能。在以后的学习路上多去阅读他们的文档,然后尝试更多其他功能,例如利用 WireMock 来进行身份验证。


技术类文章精选

非技术文章精选


↙↙↙阅读原文可查看相关链接,并与作者交流