2011年12月17日土曜日

Play 2.0 RC1 でControllerのテストを書く

この記事はPlay! framework Advent Calendar 2011 jpの17日目です。

来年の1月頃のリリースに向けて活発な開発が続いているPlay 2.0ですが、コミットログを追っていると、最近テスト関係の変更が少ないのでそろそろfixかと思い、勉強ついでにControllerのテストを書いてみました。

一応、Controllerに対するテストの書き方はPlay 2.0 RC1の公式ドキュメントにも記載されています。
https://github.com/playframework/Play20/wiki/Scalatest

テストはspecs2による記述が推奨されているようです。
具体的には、specs2にPlayのヘルパーメソッドを併用します。公式ドキュメントから抜粋したコードが以下です。

object ApplicationSpec extends Specification {

  "an Application" should {
    "execute index" in {
        withApplication(Nil, MockData.dataSource) {
          val action = controllers.Application.index
          val result = action.apply(new FakeRequest)
          ...
        }
     }
   }
}

コントローラごとに*Specを定義します。そして、
"テスト対象" should {
  "期待する振る舞い" in {
    テストコード
  }
}
という形で仕様を記述していきます。

withApplicationはアプリケーションのモックを作成して、そのモック上でコントローラなどのコンポーネントを動かしてくれます。Playに依存したコードのテストは、基本的にはwithApplicationを使った方が楽だと思われます。

MockData.dataSourceというのはただのMap[String, String]で、application.confの代わりです。次のように定義されています。
/**
 * provides standard mock data
 */
object MockData {
  val dataSource = Map("application.name" -> "mock app",
    "db.default.driver" -> "org.h2.Driver",
    "db.default.url" -> "jdbc:h2:mem:play",
    "ebean.default" -> "models.*",
    "db.default.disableJMX" -> "true",
    "mock" -> "true")
  def dataSourceAsJava = dataSource.asJava

}
デフォルト設定では、テストにインメモリデータベースを使うことがわかります。

controllers.Application.indexは、アクションを取得しています。アクションを実行しているわけではないことに注意してください。Play 1系まではコントローラのメソッドがアクションそのものでしたが、2.0のコントローラのメソッドはアクションを返すだけです。
コントローラが返したアクションをフレームワークが実際には実行するわけですが、テストにおいては自分でアクションにHTTPリクエストなどを渡して、アクションを実行する必要があります。
それが、
val result = action.apply(new FakeRequest)
の部分です。

さて、以上で公式ドキュメントの内容はおさらいできましたが、実際のテストを書くために知っておきたいことがいくつか足りていません。
例えば、リクエストメソッドやリクエストパラメータの指定、データベースの初期データの投入方法など、肝心なところが説明されていません。

というわけで、今回はPlay 2.0のzentasksサンプルの一部にテストを書いてみました。
テスト対象はApplicationコントローラ。zentasksへのログイン・ログアウトに関するアクションが定義されています。routesファイルからは次のようにマッピングされています。

# Authentication
GET     /login                              controllers.Application.login
POST    /login                              controllers.Application.authenticate
GET     /logout                             controllers.Application.logout

今回は、login, authenticate, logoutの3つのアクションの仕様を確認しつつ、テストフレームワークに足りない部分を実装しつつ、specs2の記述に落とし込んでみました。
コードはGitHubで公開してあります。

test/ApplicationSpec.scala at master from mumoshu/play20-zentasks-tests - GitHub

個別に説明していきます。まずは、loginアクションのspecを見てみてください。

"an Application" should {

    /**
     * GET /login へアクセスすると、ログインフォームが表示される。
     */
    "show login page" in {
      withEvolutions {
        withApplication(Nil, MockData.dataSource) {
          // Controllerに対するspecは、
          // 1. requestをテスト対象のactionへ渡して、resultを得る
          // 2. resultからレスポンス内容をextractする
          // 3. レスポンス内容をspecs2のMatcherで検証する
          // という流れになります。
          val action = controllers.Application.login
          val result = action.apply(FakeRequest("GET", "/login", body = AnyContentAsEmpty))
          val extracted = Extract.from(result)
          // ステータスコード
          extracted._1 must equalTo (200)
          // レスポンスヘッダ
          extracted._2.toString mustEqual ("Map(Content-Type -> text/html; charset=utf-8)")
          // レスポンスボディ
          extracted._3 must contain ("/login")
        }
      }
    }

公式ドキュメントに書いてない部分は、withEvolutionsとFakeRequest、Extractの3つですね。

まず、withEvolutionsについて説明します。そもそも、Scala版のPlayにはevolutionsという機能があります。データベースが最新の状態でないときにエラー画面でDDLの実行を促すというものです。
この便利機能が、今回は罠になりました。なんと、withApplicationでモックアプリケーションを起動するときに、evolutionsにより「データベースが最新の状態ではない」ということで例外を投げて、テストが必ず失敗してしまうのです・・・。

というわけで、テスト中にevolutionsの例外をcatchしたら、DDLを実行してから再度テストを走らせるという力技をかけました。それがwithEvolutionsです。コードが次の通りです。

private def withEvolutions[T](spec: => T): T = {
    try {
      spec
    } catch {
      case e: InvalidDatabaseRevision => {
        play.api.Logger.info("Applying all evolution scripts.")
        applyAllEvolutionScripts()
        spec
      }
    }
  }

specがwithApplicationから始まるテスト本体です。InvalidDatabaseRevisionというevolutionsの例外発生時にapplyAllEvolutionScripts()により全てのDDLを適用しています。コードは次の通りです。

private def applyAllEvolutionScripts() {
    val app = play.api.Play.current
    val api = app.plugin[DBPlugin].map(_.api).getOrElse(throw new Exception("there should be a database plugin registered at this point but looks like it's not available, so evolution won't work. Please make sure you register a db plugin properly"))
    val db = "default"
    val appPath = new File(".")
    val scripts = Evolutions.evolutionScript(api, appPath, db)
    Evolutions.applyScript(api, db, scripts)
  }

Playに組み込まれているEvolutionsPluginが行っているのと同じ方法で、"default"という名前のデータベースに必要な全てのDDLを取得・実行しています。

気づかれた方もいるかもしれませんが、正直なところ、なぜこれで動くのかわかっていない所があります・・・。本来、withApplicationでアプリケーションが作り直されるたびにインメモリデータベースも初期化されるべきだと思うのですが、どうやらされていないようです。というより、されていないから、evolutions実行後にwithApplicationをもう一度実行すると、evolutionがエラーを出さないのだと思います。@kitora_naokiさんの記事にもあるようにh2dbを使った方が良いのかもしれません。

さて、3つめのFakeRequestですが、これも今回独自に実装した、テスト用のリクエストを簡単に作成するためのクラスです。Play本体にあって欲しいところ・・・。
実装については特筆すべきところはないので説明は省いて、使い方だけ書きます。

// クエリパラメータなしのGETリクエスト
FakeRequest("GET", "/login", body = AnyContentAsEmpty)

// クエリパラメータありのGETリクエスト
FakeRequest("GET", "/login", params = Map("key" -> Seq("value")))

// POSTリクエスト
FakeRequest("POST", "/login", body = AnyContentAsUrlFormEncoded(Map(
  "email" -> Seq("mumoshu@sample.com"),
  "password" -> Seq("secret")
)))

3つめのExtractについて説明します。ExtractはPlay組み込みのクラスです。
Extract.from(result)
は、resultから
  • HTTPステータスコード
  • レスポンスヘッダ
  • レスポンスボディ
を抽出して、Tuple3で返します。

基本的にアクションのテストは、このようにresultからExtract.fromで上記3つを取り出し、specs2のmatcherで検証すれば良いでしょう。

最後に、データベースにテスト用データを投入する方法です。withApplication内で普通にModelを呼び出してinsertしましょう。
withEvolutions {
        withApplication(Nil, config) {

          User.create(User("mumoshu@sample.com", "mumoshu", "secret"))

          val action = controllers.Application.authenticate
          // つづく...

まとめ


Play 2.0 RC1でのControllerに対するテストの書き方を、公式ドキュメントより少し踏み込んで説明しました。
個人的にここで紹介した以外にも、
  • Web APIを呼び出すアクションのテスト。APIをモックする?
  • HTTP Chunkingに対応したアクションのテスト。チャンクを一つずつ取得して検証する?
  • Web Socketコントローラのテスト。
など、どうやってテストするのか気になるところが残っていますが、それはまた別の機会に。

明日はPlay! framework Advent Calendar 2011 jp 18日目、@garbagetownさんのご担当です!
よろしくお願いします。

2011年8月6日土曜日

Play framework + WebSocket + Growlで遊ぶ

第1回「Play framework勉強会」IN 関西@tan_go238さんが、NettyとGrowlを組み合わせて、指定ポートのソケットに書き込まれたメッセージをGrowlに飛ばすアプリを披露されていました。
それにインスパイアされて、Play + WebSocket + Growlを組あわせて、HTTPやWebSocket経由で送信されたメッセージをサーバのGrowlや、他のWebSocket接続ユーザへ飛ばすアプリを書いてみました。

ソースコードはこちらで公開しています。
本文には特に重要なコードしか掲載していませんので、ざっと記事を読んでいただいたあと、全体のコードを眺めて復習してみてください。
mumoshu/play-websocket-growl-sample at master - GitHub

GrowlやWebSocketでメッセージを送信する


public class Notifier {
    /* 略 */

    public static void notify(String message) {
        Logger.info("Notify: %s", message);
        growl().notify("system", "Play feat. Growl", message);
        NotificationStream.publish(message);
    }
}

HTTPやWebSocketから送信されたメッセージは、Notifierというクラス経由で他のWebSocketクライアントやサーバのGrowlへ飛ばします。
このコードでは、
growl().notify(...)
でサーバへGrowlによる通知を送信しつつ、
NotificationStream.publish(message)
により接続中のWebSocketクライアントへメッセージを送信します。

これを、HTTP POSTされたときやWebSocketクライアントからのメッセージを受信した際に呼び出します。

HTTP経由


HTTPやWebSocketからはそれぞれこんな感じで受け取ったメッセージをNotifierに渡します。

public class Application extends Controller {
 
    public static void growlHTTP() {
        render();
    }

    public static void growlWS() {
        render();
    }

    public static void notifyWithGrowl(String message) {
        Notifier.notify(message);
    }
}

growlHTTPがHTTP経由で、growlWSがWebSocket経由でメッセージをサーバへ送信するページを表示するアクションです。
HTTP経由のメッセージ送信は、notifyWithGrowlへHTTP POSTを送信することで行います。

WebSocket経由


WebSocketクライアントからのメッセージ送受信はこのように行います。

public class Notifications extends Controller {
    public static void index() {
        render();
    }

    public static class WebSocket extends WebSocketController {

        public static void connect() {
            Logger.info("Connected.");

            F.EventStream eventStream = NotificationStream.stream();

            while(inbound.isOpen()) {
                F.Either receivedEvent = await(F.Promise.waitEither(
                        inbound.nextEvent(),
                        // 以下のようにするとNotificationが無限に返ってきてアプリが応答しなくなります。
                        // F.EventStream eventStream = NotificationStream.stream();
                        eventStream.nextEvent()
                ));

                // WebSocketから受け取ったメッセージをGrowlや他のWebSocketコネクションにブロードキャストする。
                for (String message : TextFrame.match(receivedEvent._1)) {
                    Notifier.notify(message);
                    Logger.info("Message received: %s", message);
                }

                // 他のWebSocket接続から受け取ったメッセージを、現在のWebSocket接続先に送る。
                for (Notification notification : ClassOf(Notification.class).match(receivedEvent._2)) {
                    Logger.info("Matched to Notification class.");
                    outbound.send(notification.message);
                }

                for (Http.WebSocketClose close : SocketClosed.match(receivedEvent._1)) {
                    disconnect();
                }
            }
        }
    }
}

この例ではEventStreamは、PlayアプリからWebSocketクライアントと間接的にメッセージ交換をするためのパイプの役割を持ちます。
Playアプリと全WebSocketクライアントが通信するための「バス」もしくは「ハブ」とも言えます。

通信とデータの流れとしてはこうです。

Application.notifyWithGrowl()のリクエスト
または
Notifications.WebSocket.connect()でWebSocketのメッセージ受信
↓
Notifier.notify(message)
↓
ArchivedEventStream#publish(new Notification(message))
↓
Notificationが「接続中の全WebSocketクライアントの」
Notifications.WebSocket.connect()におけるeventStream.nextEvent()から返ってくる。

あるHTTP/WebSocketクライアントのリクエストやジョブから、別のコネクションを貼っているWebSocketクライアントにメッセージを送る場合、EventStreamを経由する以外に方法はありません。

まとめ

  • HTTPやWebSocket経由でメッセージを受信しました
  • 受信したメッセージをサーバへGrowl通知しつつ、他のWebSocketクライアントへ送信しました。
基本的には誰得でございますが、
スライド発表中にこういうアプリでリアルタイムにコメントをもらうと面白いかもしれませんw

参考


Growl notifications in Java with script engine and AppleScript | Jayway Team Blog - Sharing Experience

Play frameworkのchatサンプル

2011年7月25日月曜日

Scala で Functional Dependency

Functional Dependencies in Scala
の自分なりの解釈をメモ。

Functional Dependencyとは?


Haskellで標準的に使われているパターンらしい。

Functional dependencies are used to constrain the parameters of type classes. They let you state that in a multi-parameter type class, one of the parameters can be determined from the others, so that the parameter determined by the others can, for example, be the return type but none of the argument types of some of the methods.

Quoted from: Functional Dependencies

Functional Dependencyは、型クラスのパラメータに制約をかけるために利用します。
例えば、複数の型パラメータを持つ型クラスについて、あるパラメータによって別のパラメータが決まる、という制約を表現することができます。
これを応用してメソッドを定義すると、メソッドの引数に無い型を戻り型にする、ということができます。

Java屋さん的に言えば、型クラスを、型安全にDependency Injectionする仕組みです。

Scalaでのコード例


Functoinal Dependencyは長いので、ここではfundepと略して説明します。

// 型クラスのパラメータとなる型
trait Matrix
trait Vector

// fundepの説明における"型クラスのパラメータに制約をかける"部分に対応します.
// MultiDepという型クラスのパタメータA, B, Cに制約をかけます.
trait MultDep[A, B, C]
// trait MultDep[-A, -B, +C]    // for Scala <= 2.9.0-1 these variance
//                              // annotations are required

// fundepの説明における
//  "複数の型パラメータを持つ型クラスについて、あるパラメータによって別のパラメータが決まる、とう制約を表現"
// しているのがここです.
//
// パラメータA, Bによって, 別のパラメータCが決まる, という制約を表現していると考えてください.
//
// 逆に言えば, ここで記述されていないパラメータの組み合わせは許さないのでコンパイルエラーにしたい, ということになります.

// A, BがMatrixなら, CはMatrixである
implicit object mmm extends MultDep[Matrix, Matrix, Matrix]
// AがMatrix, BがVectorなら、CはVectorである
implicit object mvv extends MultDep[Matrix, Vector, Vector]
// 以下、同様に。
implicit object mim extends MultDep[Matrix, Int, Matrix]
implicit object imm extends MultDep[Int, Matrix, Matrix]

// 利用例

implicitly[MultDep[Matrix, Matrix, Matrix]]
// ABCが制約を満たしていれば何らかのオブジェクトが返ってくる。
// 満たしていなければコンパイルエラー

// fundepの説明 "メソッドの引数に無い型を戻り型にする" に対応する例が以下です.
// メソッドの引数A, B以外の型Cを戻り型に指定しています.
// A, B, Cの組み合わせが上記のimplicit objectで表現されていないとコンパイルエラーになるわけです.
def mult[A, B, C](a : A, b : B)(implicit instance : MultDep[A, B, C]) : C = /** instanceのメソッドを呼び出すコード */

// これはコンパイル成功する
val r1 : Matrix = mult(new Matrix {}, new Matrix{})
val r2 : Vector = mult(new Matrix {}, new Vector{})
val r3 : Matrix = mult(new Matrix {}, 2)
val r4 : Matrix = mult(2, new Matrix {})

// これはコンパイル失敗する
// implicit object mvm extends MultDep[Matrix, Vector, Matrix]
// のようなimplicit objectが定義されていないので,
// 「MultiDep[Matrix, Vector, Matrix]のようなimplicit objectが見つかりません」というコンパイルエラーが出力される.
val r5 : Matrix = mult(new Matrix {}, new Vector{})

fundepの適用範囲

Functional Dependecyを利用すると、関数に対して、複数のパラメータを持つ型クラスを型安全にDIできるわけですが、実際にどういった実装に応用していくか?
関数の型パラメータによって処理が異なったり、型パラメータの組み合わせに制約がある場合に応用するとよさそうです。

関数の型パラメータの制約を、関数内の条件分岐として実装して、実行時に制約違反を発見するか、もしくはコンパイル時にコンパイルエラーとして出力するか?
コンパイル時にコンパイルエラーとして出力するほうが良いですよね。
Functional dependecyが使えます。

関数の型パラメータの組み合わせによる処理の違いを、関数内に実装するか、関数外の型クラスのinsntanceどちらに実装すると見通しや拡張性が良いか?
型クラスのinstanceですよね。
ここでもFunctional dependecyが使えるということです。

まとめ

  • Functional DependecyはHaskellで標準的に使われるパターン
  • 型クラスの型パラメータに制約をかけることができる
  • 関数の型パラメータによって処理が異なるとき、異なる部分を型クラスのインスタンスのメソッドとして抜き出すことで、コードの見通しを良くできる

参考


Thanks to:

Blogger でSyntax Highlighterを使う

Syntax Highlighter v3を Blogger で使いたかったので、以下のサイトにあるジェネレータを利用させていただきました。

How to Add Syntax Highlighter(v3) to Blogger Blogs ~ Blogger Widgets | Tips | Trick | Hacks | Help! ~ Way2Blogging

テーマと対応する言語をチェックボックスで選んで、「Get Code」ボタンを押し、表示されたHTMLコードをコピーしてください。
それから、Bloggerのテンプレートのhead閉じタグの直前にペーストするだけで設定完了です。
後は、投稿の編集中に、



を記述すれば利用できます。