はじめに
これは下記 playframework のドキュメントの翻訳です。
ws クライアントをテストする
ほとんどのアプリケーションは、他の Web サービスの API 等を利用するクライアントを実装する。
この API クライアント等は、JSON 等の単なる文字列でやり取りするため、型付けが弱くなりがちである。
そのため、テストは重要である。
このような API クライアントをテストする方法は、いくつかのパターンかある。
本当に Web サービスに対してリクエストを送る
テスト内で、本当に Web サービスの API に対してリクエストする方法がある。
この方法だと、実際にリクエストを送るので、テストの信頼度はかなり高い。
一方で、以下の理由により、あまり実用的ではないのが事実である。
- API Limit などの制限に引っかかる事がある
- Web サービスの挙動を自由にいじるのは難しいので、テストで確認したいデータが用意できないことがある
Web サービスのテスト環境に対してリクエストを送る
Web サービスによっては、開発用のテスト環境を用意してくれている場合があるので、テストではここに対してリクエストする。
先程の、実際の Web サービスでテストするよりはマシかもしれないが、以下のような弊害がある。
- テスト用の環境を用意してくれていないサービスもある
- テスト用の環境が落ちているとテストも落ちてしまう
HTTP クライアントをモックする
HTTP クライアント部分をモックに差し替える方法があるが、これだと
- 単にコードが動くかどうかのテストになってしまいがち
- 本当に正しいリクエストが送られているのかどうか、テストができていない
Web サービスのモックを作成する
自分たちで、 Web サービスのモックする方法もある。
Play には、このモックを簡単に作る仕組みが用意されている。
Web サービスのモックを立ち上げてテストを実行する方法には以下のメリットがある。
- 正しく HTTP リクエストが行われていることを確認できる
- JSON シリアライズ/デシリアライズも含めて動作確認できる
- 外部サービスには一切依存しないでテストができる
最初に述べたように、シリアライス/デシリアライズの部分は、型の縛りが弱くて、バグが出やすい部分であるので、このテストは有用である。
GitHub Client をテストする例
例として、 GitHub Client をテストしてみる。
以下のような、GitHub の public repository を取得するだけのコードだ。
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) { @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 の baseURL が外から注入できるようになっていることである。
これを上書きすることでモックサーバーにリクエストを向ける。
続いて、Web サービスのモックを実装する。 Server
と withRouter
ヘルパーを使って、以下の様に実装する。
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
メソッドには、リクエストに対する処理のコードを渡してやる。サーバーを立ち上げるポートを渡すこともできるが、特に何も指定しなければ、自動で開いてるポートで起動してくれる。
最終的に implicit port で、立ち上がったポート番号が返ってくるので、これをテストコードに伝えてあげる。
これで準備ができたので、テストコードを書く。
WsTestClient.withClient
というメソッドで、特殊な WsClient
を生成できる。
先程、GitHub Client の baseUrl
が外から注入できるのが重要だ、という話をした。テスト用の WsClient
は、 host を指定しなければ、 localhost の適切なポートに対してリクエストしてくれるため、 baseUrl
を空にして、PATH の部分だけでリクエストするようにしてやる。
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") } } } } }
レスポンスをファイルで用意する
先程は最低限のレスポンスを用意したが、実際のレスポンスと同じ形式のレスポンスでテストしたい場合がある。
Play にある sendResource
メソッドを使えば、ファイルからレスポンスを生成して返すことができる。
まずはファイルを作成する。play デフォルトのディレクトリ構造であれば test/resources
ディレクトリ、 sbt デフォルトのディレクトリ構造を使っているなら src/test/resources
に配置する。
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" } ]
テストコードは以下のようになる
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") }