wsで他サービスにAPIリクエストしている部分のテスト
Testing web service clients
Test against the actual web service - 実際にWebサービスでテストする場合
APIリミットに引っかかるなどの弊害がある
テストで確認したいデータが用意できないことがある
Test against a test instance of the web service - テスト用のWebサービスをでテストする
上(実際にWebサービスでテストする)よりはまし
Webサービスの中にはテスト用の環境を用意していないものもある
テスト用の環境があったとしても、その環境が落ちてたりするとテストも落ちる
Mock the http client - HTTPクライアントをモックする
単に、コードが動くかどうかのテストになり、価値がない
HTTP リクエストが正しく送られているかどうかはテストできない
Mock the web service - Webサービスをモックする
実際のWebサービスのリクエストする場合と、HTTPクライアントのモックをする場合の、間をとった良いアプローチである
正しくHTTPリクエストが行われていることを確認できる
シリアライズとかデシリアライズも含めて
シリアライズはデシリアライズは、型の縛りが弱くてバグりやすいと序盤にかいてある(scrapboxでは省略してしまった)
第三者のサービスに依存していない
Play では Web サービスをモックするような仕組みが用意されている
Testing a GitHub client
例として GitHub client のテストを書いてテストしてみる。
クライアントのコードは以下の通り。パブリックリポジトリの名前を取得するコード。
code:scala
import javax.inject.Inject
import play.api.libs.ws.WSClient
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
class GitHubClient(ws: WSClient, baseUrl: String)(implicit ec: ExecutionContext) {
def repositories(): Future[SeqString] = { ws.url(baseUrl + "/repositories").get().map { response =>
(response.json \\ "full_name").map(_.asString).toSeq }
}
}
GitHub API の baseUrl がパラメータになっているところに注目。
この部分を上書きしてモックサーバーに向ける。
このエンドポイントを実装した、内蔵 Play サーバーを実装する。
Server と withRouter を使ってこのように実装する。
code:scala
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 メソッドは、リクエストに対する処理のコードと、サーバーが立ち上がるポートを受け取る。
デフォルトだと、自動で空いているポートで起動する
どのポートを割り当てるかはあまり気にしなくて良い
ただし、どのポートを使っているかはコードに伝えてやる必要がある
準備ができたので GitHub client をテストする
Play には WsTestClient トレイトがある。
WsTestClient.withClient メソッドで特殊な WsClient を生成できる
パスを渡すとデフォルトでホスト名が localhost になる
そのため、ホスト名は空文字にしておけば良い
ポート番号は暗黙に設定される
code:scala
import play.core.server.Server
import play.api.routing.sird._
import play.api.mvc._
import play.api.libs.json._
import play.api.test._
import scala.concurrent.Await
import scala.concurrent.duration._
import org.specs2.mutable.Specification
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")
}
}
}
}
}
Returning files - レスポンスをファイルで用意しておくこともできます
実際のレスポンスと同様のレスポンスでテストしたい場合がある。
Play の sendResource メソッドを使えば、ファイルからレスポンスを生成して返すことができる。
まずファイルを作成する。
もし play のディレクトリ構造をそのまま使っていたら、 test/resources ディレクトリにファイルを置く。
sbt デフォルトのディレクトリ構造を使っているなら src/test/resources になる
github/repositories.json というファイルを以下の内容で作成するとする
code:json
[
{
"id": 1296269,
"owner": {
"login": "octocat",
"id": 1,
"gravatar_id": "",
"type": "User",
"site_admin": false
},
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"description": "This your first repo!",
"private": false,
"fork": false,
}
]
// 各レスポンスから https://api.github.com を削除すれば、...
テストコードは以下のようになる
code:scala
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 でレスポンスを返す。
setup code を分ける
以下のように withGitHubClient メソッドを準備しておくと、テストが増えた時に再利用できて便利である。
code:scala
import play.api.mvc._
import play.api.routing.sird._
import play.core.server.Server
import play.api.test._
def withGitHubClientT(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, ""))
}
}
}
このメソッドの使い方はこうである
code:scala
withGitHubClient { client =>
val result = Await.result(client.repositories(), 10.seconds)
result must_== Seq("octocat/Hello-World")
}
実際に実装してみる
code:sh
error /Users/yoshiyuki_sakamoto/lgtmoon/test/test/feature/ImageSearchTest.scala:32:16: value GET is not a case class, nor does it have a valid unapply/unapplySeq member error case GET(p"/customsearch/v1") => components.defaultActionBuilder { なんか import が漏れている気がする
PlaySpecification に GET が実装されていて、名前が被っているためっぽい
trait HttpVerbs
trait RequestMethodExtractors
両方に GET が定義されていて被っている?!