ドキュメント

§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サーバーが必要です。ServerwithRouterヘルパーと文字列補間ルーティング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でリクエストを行った後、テストリソースディレクトリに保存するファイルを作成します。テストリソースディレクトリは、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"
  }
]

テストのニーズに合わせて変更することを選択できます。たとえば、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のコンテンツタイプを設定します。

§設定コードの抽出

これまで実装してきたテストは、実行したいテストが1つしかない場合は問題ありませんが、テストしたいメソッドが多数ある場合は、モッククライアントの設定コードを1つのヘルパーメソッドに抽出する方が理にかなっている場合があります。たとえば、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")
}

次へ: ロギング


このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に送ってください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。