Scalatraアプリケーションをテストする

[Scalatra](http://scalatra.org)のテストに関する公式ドキュメントがあまり充実していないので、書いてみました。

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

WebアプリケーションのE2E(End to End)テストと言えば[Selenium](http://www.seleniumhq.org)や[Headless Crome](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md)のようなヘッドレスブラウザを使った方法が定番ですが、Scalaのツール類とはまったく異なる環境を用意する必要が有り、手軽に実行するには少々難が有ります。

そこで[Scalatra](http://scalatra.org)には`scalatra-test`という、ScalatraアプリケーションのE2Eテストをサポートするライブラリが用意されていて、Scala用のメジャーなテスティングフレームワークである[ScalaTest](http://www.scalatest.org)や[specs2](https://etorreborre.github.io/specs2/)の記法を使ってテストを記述できます。

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

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

[ScalaTest](http://www.scalatest.org)に馴染みにある方であれば、上記のテストコードが[ScalaTest](http://www.scalatest.org)の[FunSuite](http://www.scalatest.org/getting_started_with_fun_suite)というテストスタイルを使って記述されていることが分かることでしょう。

[ScalaTest - Getting started with FunSuite](http://www.scalatest.org/getting_started_with_fun_suite)

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

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

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

`scalatra-test`は[Apache HttpComponents](https://hc.apache.org)の[HTTPクライアント](https://hc.apache.org/httpcomponents-client-ga/index.html)機能をバックエンドに使って実装されているため、ChromeやSafariなどのブラウザの挙動を再現できるわけではありません(JavaScriptの実行機能などはありません)。

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

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

# サンプルコード

では、実際に`scalatra-test`を使ったE2Eテストの例を見ていきましょう。ここでは[ScalaTest](http://www.scalatest.org)を使います。

## プロジェクトの作成

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

```sh
$ sbt new scalatra/scalatra.g8
```

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

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

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

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


```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. パッケージのインポート

     ```scala
     import org.scalatra.test.scalatest._
     ```

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

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

    ```scala
    class MyScalatraServletTests extends ScalatraFunSuite
    ```

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

    `FlatSpec`や`FunSpec`など、scalatestが用意する別のテストスタイルも使えますし、Specs2であれば`Acceptance specification`と`Unit specification`の両方の記法が使えます。

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

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

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

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

 4. テストを用意する

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

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

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

    ```scala
    get("/") { }
    ```

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

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

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

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

    ```scala
    {
        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!!