文档

§测试 Web 服务客户端

编写 Web 服务客户端需要大量的代码 - 准备请求、序列化和反序列化主体、设置正确的标头。由于很多代码都与字符串和弱类型映射一起使用,因此测试它非常重要。但是测试它也带来了一些挑战。一些常见的方法包括

§针对实际 Web 服务进行测试

当然,这会对客户端代码产生最高级别的信心,但是通常不切实际。如果它是第三方 Web 服务,可能存在速率限制,以防止您的测试运行(并且针对第三方服务运行自动化测试不被认为是良好的网络公民)。可能无法在该服务上设置或确保您的测试所需的必要数据的存在,并且您的测试可能会对该服务产生不良的副作用。

§针对 Web 服务的测试实例进行测试

这比前一个方法好一点,但是它仍然存在一些问题。许多第三方 Web 服务不提供测试实例。这也意味着您的测试依赖于测试实例正在运行,这意味着测试服务可能会导致您的构建失败。如果测试实例位于防火墙后面,它还会限制测试可以从哪里运行。

§模拟 HTTP 客户端

这种方法对测试代码的信心最低 - 通常这种测试仅限于测试代码是否按预期执行,这毫无价值。针对模拟 Web 服务客户端的测试表明代码运行并执行某些操作,但不能保证代码执行的任何操作是否实际上与正在进行的有效 HTTP 请求相关联。

§模拟 Web 服务

这种方法是在实际 Web 服务上进行测试和模拟 HTTP 客户端之间的一个很好的折衷方案。您的测试将表明它发出的所有请求都是有效的 HTTP 请求,主体序列化/反序列化工作正常,等等,但它们将完全自包含,不依赖于任何第三方服务。

Play 提供了一些用于在测试中模拟 Web 服务的辅助工具,使这种测试方法成为一个非常可行且有吸引力的选择。

§测试 GitHub 客户端

例如,假设您编写了一个 GitHub 客户端,并且想要对其进行测试。该客户端非常简单,它只允许您查找公共存储库的名称

import com.fasterxml.jackson.databind.JsonNode;
import java.util.*;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import javax.inject.Inject;
import play.libs.ws.WSClient;

class GitHubClient {
  private WSClient ws;

  @Inject
  public GitHubClient(WSClient ws) {
    this.ws = ws;
  }

  String baseUrl = "https://api.github.com";

  public CompletionStage<List<String>> getRepositories() {
    return ws.url(baseUrl + "/repositories")
        .get()
        .thenApply(
            response ->
                response.asJson().findValues("full_name").stream()
                    .map(JsonNode::asText)
                    .collect(Collectors.toList()));
  }
}

请注意,它将 GitHub API 基本 URL 作为参数 - 我们将在测试中覆盖它,以便我们可以将其指向我们的模拟服务器。

为了测试这一点,我们需要一个嵌入式 Play 服务器来实现此端点。我们可以通过 创建嵌入式服务器路由 DSL 来实现。

Server server =
    Server.forRouter(
        (components) ->
            RoutingDsl.fromComponents(components)
                .GET("/repositories")
                .routingTo(
                    request -> {
                      ArrayNode repos = Json.newArray();
                      ObjectNode repo = Json.newObject();
                      repo.put("full_name", "octocat/Hello-World");
                      repos.add(repo);
                      return ok(repos);
                    })
                .build());

我们的服务器现在正在一个随机端口上运行,我们可以通过 httpPort 方法访问它。我们可以使用它构建要传递给 GitHubClient 的基本 URL,但是 Play 具有更简单的机制。 WSTestClient 类提供了一个 newClient 方法,该方法接受一个端口号。当使用客户端向相对 URL(例如 /repositories)发出请求时,此客户端会将该请求发送到传递的端口上的 localhost。这意味着我们可以在 GitHubClient 上将基本 URL 设置为 ""。这也意味着如果客户端返回带有指向其他资源的 URL 链接的资源,然后客户端使用这些链接发出进一步的请求,我们可以确保这些链接是相对 URL 并按原样使用它们。

因此,我们现在可以在 @Before 注释的方法中创建服务器、WS 客户端和 GitHubClient,并在 @After 注释的方法中关闭它们,然后我们可以在测试中测试客户端

import static org.hamcrest.core.IsCollectionContaining.*;
import static org.junit.Assert.*;
import static play.mvc.Results.*;

import com.fasterxml.jackson.databind.node.*;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import play.libs.Json;
import play.libs.ws.*;
import play.routing.RoutingDsl;
import play.server.Server;

public class GitHubClientTest {
  private GitHubClient client;
  private WSClient ws;
  private Server server;

  @Before
  public void setup() {
    server =
        Server.forRouter(
            (components) ->
                RoutingDsl.fromComponents(components)
                    .GET("/repositories")
                    .routingTo(
                        request -> {
                          ArrayNode repos = Json.newArray();
                          ObjectNode repo = Json.newObject();
                          repo.put("full_name", "octocat/Hello-World");
                          repos.add(repo);
                          return ok(repos);
                        })
                    .build());
    ws = play.test.WSTestClient.newClient(server.httpPort());
    client = new GitHubClient(ws);
    client.baseUrl = "";
  }

  @After
  public void tearDown() throws IOException {
    try {
      ws.close();
    } finally {
      server.stop();
    }
  }

  @Test
  public void repositories() throws Exception {
    List<String> repos = client.getRepositories().toCompletableFuture().get(10, TimeUnit.SECONDS);
    assertThat(repos, hasItem("octocat/Hello-World"));
  }
}

§返回文件

在前面的示例中,我们手动构建了模拟服务的 json。通常,最好捕获您正在测试的服务的实际响应,并返回它。为了帮助您实现这一点,Play 提供了一个 sendResource 方法,该方法允许您轻松地从类路径上的文件创建结果。

在对实际的 GitHub API 发出请求后,创建一个文件将其存储在测试资源目录中。测试资源目录是 test/resources(如果您使用的是 Play 目录布局)或 src/test/resources(如果您使用的是标准 sbt 目录布局)。在本例中,我们将将其命名为 github/repositories.json,它将包含以下内容

[
  {
    "id": 1296269,
    "owner": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "name": "Hello-World",
    "full_name": "octocat/Hello-World",
    "description": "This your first repo!",
    "private": false,
    "fork": false,
    "url": "https://api.github.com/repos/octocat/Hello-World",
    "html_url": "https://github.com/octocat/Hello-World"
  }
]

您可以根据自己的测试需求对其进行修改,例如,如果您的 GitHub 客户端使用上述响应中的 URL 向其他端点发出请求,您可能需要从这些 URL 中删除 https://api.github.com 前缀,以便它们也成为相对 URL,并由测试客户端自动路由到 localhost 的正确端口。

现在,修改路由器以提供此资源

Server server =
    Server.forRouter(
        (components) ->
            RoutingDsl.fromComponents(components)
                .GET("/repositories")
                .routingTo(request -> ok().sendResource("github/repositories.json"))
                .build());

请注意,由于文件名扩展名为 .json,Play 将自动设置 application/json 的内容类型。

下一步:日志记录


发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区展开对话。