Scalatraのテストに関する公式ドキュメントがあまり充実していないので、書いてみました。

ScalatraアプリケーションのE2Eテスト

WebアプリケーションのE2E(End to End)テストと言えばSeleniumHeadless Cromeのようなヘッドレスブラウザを使った方法が定番ですが、Scalaのツール類とはまったく異なる環境を用意する必要が有り、手軽に実行するには少々難が有ります。

そこでScalatraにはscalatra-testという、ScalatraアプリケーションのE2Eテストをサポートするライブラリが用意されていて、Scala用のメジャーなテスティングフレームワークであるScalaTestspecs2の記法を使ってテストを記述できます。

以下のコードは、GETメソッドを使ってroot path("/")にアクセスするとHTTPのステータスコード200が返却されることをテストします。

test("GET / on MyScalatraServlet should return status 200"){
  get("/"){
    status should equal (200)
  }
}

ScalaTestに馴染みにある方であれば、上記のテストコードがScalaTestFunSuiteというテストスタイルを使って記述されていることが分かることでしょう。

ScalaTest - Getting started with FunSuite

テストの実行も通常のScalaのユニットテストと同様にsbtから行います。

$ sbt test

このようにscalatra-testは、既存のScalaのツールやテスティングフレームワークを使って素早くE2Eテストを書けるところが最大の特徴です。

scalatra-testの'実装'と、'使いどころ'

scalatra-testApache HttpComponentsHTTPクライアント機能をバックエンドに使って実装されているため、ChromeやSafariなどのブラウザの挙動を再現できるわけではありません(JavaScriptの実行機能などはありません)。

あくまでHTTPリクエストを送信し、受信したHTTPレスポンスの内容をテストするだけです(PerlのPlack::Testや、RubyのRack::Testに近いイメージ)。しかしながらCookieを始めとするサーバサイドアプリケーションの動作確認に必要なクライアント機能は一通りサポートされているので、ログインや画面遷移を含む基本的なアプリケーションの動作検証や、APIサーバのテストなどには充分な機能を備えていると言えるでしょう。

'サーバサイドアプリケーションの挙動'を確認するテストはscalatra-test、'フロントエンドも含めたアプリケーション全体の挙動'を確認するテストはSeleniumやヘッドレスブラウザベースのテスティングツールを使う、といった方法が良いでしょう。

# サンプルコード

では、実際にscalatra-testを使ったE2Eテストの例を見ていきましょう。ここではScalaTestを使います。

プロジェクトの作成

scalatraのプロジェクトテンプレートからプロジェクトを作ります。

$ sbt new scalatra/scalatra.g8

生成されたbuild.sbtにはScalaTestを使ったテストに対応するためにscalatra-scalatestというパッケージが記述されています。

  "org.scalatra" %% "scalatra-scalatest" % "2.6.2" % "test",

scalatra-scalatestscalatra-testに依存していて、その依存関係は自動的にsbtが解決してくれるので、明示的なscalatra-testパッケージの指定は不要です。

実際のテストは、src/test/scala/com/example/app/MyScalatraServletTests.scalaに記述されています。

package com.example.app

import org.scalatra.test.scalatest._

class MyScalatraServletTests extends ScalatraFunSuite {

  addServlet(classOf[MyScalatraServlet], "/*")

  test("GET / on MyScalatraServlet should return status 200"){
    get("/"){
      status should equal (200)
    }
  }

}

ScalaTestとspecs2ではテスト記法は異なりますが、テストを実行するまでの基本的な流れは同一です。順に解説していきます。

  1. パッケージのインポート

     import org.scalatra.test.scalatest._
    

    scalatra-scalatestを有効にします。Specs2を使う場合は、org.scalatra.test.specs2._を使います。

  2. scalatestまたはspecs2用のtraitを継承したテストクラスを用意する

    class MyScalatraServletTests extends ScalatraFunSuite
    

    コード例ではscalatestが複数用意しているテストスタイルのうち、FunSuiteを使ったスタイルを選択するためにScalatraFunSuiteを継承しています。

    FlatSpecFunSpecなど、scalatestが用意する別のテストスタイルも使えますし、Specs2であればAcceptance specificationUnit specificationの両方の記法が使えます。

  3. テスト対象のservletを指定する

    addServlet(classOf[MyScalatraServlet], "/*")
    

    ScalatraBootstrapの中でServletを指定するときと同様に、パス名と共にservletのクラスを指定します。new MyScalatraServletのようにオブジェクトを直接渡してもOKです。

    servletだけでなく、servletFilterも指定できます。

  4. テストを用意する

    test("GET / on MyScalatraServlet should return status 200")
    

    テストを用意します。ScalaTestではメソッド定義の形式を取っていないので少し分かりづらいですが、ScalatraFunSuitetestメソッドを使ってテスト名とテストコードを「登録」しておく仕組みになっています。class定義の中ですぐに実行されるわけではありません。

  5. HTTPメソッドでリクエスト条件(パス、パラメータ等)を指定する

    get("/") { }
    

    ここが一番のポイントとなる箇所です。Scalatraのルーティング定義と同じような記法ですが、ここではリクエストのメソッドとパスを指定しています。ホスト名は指定せずに、パス名だけを指定します。

    パス名に自前でパラメータを連結しても正しく動作しますが、分かりづらいので、get(uri = "/", params = Seq("header1", "param1"))のように、uriとは別引数でパラメータを渡すこともできます。

    ここも色々な呼び出し方がサポートされていますので、後ほど詳しく解説します。

  6. レスポンスの内容を検証し、テストの成否を判定する

    {
        status should equal (200)
    }
    

    status, header, bodyといった主要な要素が引き渡されるので、コードブロックの中でテスティングフレームワークが提供するマッチャーを使って、レスポンスの正当性を判定します。

    ここではstatusが200,つまり正常にレスポンスが返ってきたことを判定しています。

    コードブロックの返り値がそのままテスト結果になるので、マッチャーによる判定はコードブロックの一番最後に書きます。

    また、ScalaTestの場合、Matcher traitが予め有効になっているので、should 〜という書き方が追加のimport無しにできるようになっています(通常のScalaTestでは明示的にimportする必要が有ります)。

上記のテストをsbtから、testタスクで実行すると、下記のような表示がされて、テストの成否が分かります。

[info] MyScalatraServletTests:
[info] - GET / on MyScalatraServlet should return status 200
[info] Run completed in 1 second, 284 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 2 s, completed Dec 30, 2017 10:51:59 PM

更に詳しい使い方は後日、続きのエントリで解説します。

Enjoy Testing!!