§测试 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")
}
下一步:日志记录
在此文档中发现错误?此页面的源代码可以在 此处 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?转到 我们的社区论坛 与社区开始对话。