文档

§测试 Web 服务客户端

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

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

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

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

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

§模拟 HTTP 客户端

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

§模拟 Web 服务

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

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

§测试 GitHub 客户端

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

import javax.inject.Inject

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import play.api.libs.ws.WSClient

class GitHubClient(ws: WSClient, baseUrl: String)(implicit ec: ExecutionContext) {
  @Inject def this(ws: WSClient, ec: ExecutionContext) = this(ws, "https://api.github.com")(ec)

  def repositories(): Future[Seq[String]] = {
    ws.url(baseUrl + "/repositories").get().map { response => (response.json \\ "full_name").map(_.as[String]).toSeq }
  }
}

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

为了测试这一点,我们需要一个嵌入式 Play 服务器来实现此端点。我们可以使用 Server withRouter 辅助方法结合 字符串插值路由 DSL 来实现。

import play.api.libs.json._
import play.api.mvc._
import play.api.routing.sird._
import play.core.server.Server

Server.withRouterFromComponents() { components =>
  import Results._
  import components.{ defaultActionBuilder => Action }
  {
    case GET(p"/repositories") =>
      Action {
        Ok(Json.arr(Json.obj("full_name" -> "octocat/Hello-World")))
      }
  }
} { implicit port =>

withRouter 方法接受一个代码块,该代码块将服务器启动的端口号作为输入。默认情况下,Play 在随机的空闲端口上启动服务器 - 这意味着您无需担心构建服务器上的资源争用或为测试分配端口,但这意味着您的代码需要知道将使用哪个端口。

现在要测试 GitHub 客户端,我们需要一个 WSClient。Play 提供了一个 WsTestClient 特性,它有一些用于创建测试客户端的工厂方法。withClient 接受一个隐式端口,这在与 Server.withRouter 方法结合使用时非常方便。

这里 WsTestClient.withClient 方法创建的客户端是一个特殊的客户端 - 如果您给它一个相对 URL,那么它将默认主机名为 localhost,端口号为隐式传入的端口号。利用这一点,我们可以简单地将 GitHub 客户端的基 URL 设置为空字符串。

将所有内容放在一起,我们有以下内容

import scala.concurrent.duration._
import scala.concurrent.Await

import org.specs2.mutable.Specification
import play.api.libs.json._
import play.api.mvc._
import play.api.routing.sird._
import play.api.test._
import play.core.server.Server

class GitHubClientSpec extends Specification {
  import scala.concurrent.ExecutionContext.Implicits.global

  "GitHubClient" should {
    "get all repositories" in {
      Server.withRouterFromComponents() { components =>
        import Results._
        import components.{ defaultActionBuilder => Action }
        {
          case GET(p"/repositories") =>
            Action {
              Ok(Json.arr(Json.obj("full_name" -> "octocat/Hello-World")))
            }
        }
      } { implicit port =>
        WsTestClient.withClient { client =>
          val result = Await.result(new GitHubClient(client, "").repositories(), 10.seconds)
          result must_== Seq("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 向其他端点发出请求,您可能会从中删除 https://api.github.com 前缀,以便它们也是相对的,并将自动由测试客户端路由到 localhost 上的正确端口。

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

import play.api.mvc._
import play.api.routing.sird._
import play.api.test._
import play.core.server.Server

Server.withApplicationFromContext() { context =>
  new BuiltInComponentsFromContext(context) with HttpFiltersComponents {
    override def router: Router = Router.from {
      case GET(p"/repositories") =>
        Action { req => Results.Ok.sendResource("github/repositories.json")(executionContext, fileMimeTypes) }
    }
  }.application
} { implicit port =>

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

§提取设置代码

如果您只想运行一个测试,那么到目前为止实现的测试就可以了,但是如果您有许多要测试的方法,那么将模拟客户端设置代码提取到一个辅助方法中可能更有意义。例如,我们可以定义一个 withGitHubClient 方法

import play.api.mvc._
import play.api.routing.sird._
import play.core.server.Server
import play.api.test._

def withGitHubClient[T](block: GitHubClient => T): T = {
  Server.withApplicationFromContext() { context =>
    new BuiltInComponentsFromContext(context) with HttpFiltersComponents {
      override def router: Router = Router.from {
        case GET(p"/repositories") =>
          Action { req => Results.Ok.sendResource("github/repositories.json")(executionContext, fileMimeTypes) }
      }
    }.application
  } { implicit port => WsTestClient.withClient { client => block(new GitHubClient(client, "")) } }
}

然后在测试中使用它看起来像这样

withGitHubClient { client =>
  val result = Await.result(client.repositories(), 10.seconds)
  result must_== Seq("octocat/Hello-World")
}

下一步:日志记录


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