Play Frameowrk2.x Javaのテスト入門 ~Controller, View編〜

Advent Calendarは終わってしまいましたが、今年中になんとかplay!2.x Javaのテスト入門の続きを書きます。

Controllerのテストの難しさ

初っ端からですが、play!のControllerのテストはどうやって書けばいいかとても困りました。Controllerは、出力としてResult型のオブジェクトを返します。このResult型の中身は、httpステータスコードだけでなくViewに渡された処理結果のコンテンツ(html)が入っています。
ControllerをテストするときはViewに渡す引数オブジェクトをインターセプトしてテストできると良いのですが、残念ながら出来ません。そのためassertThat(contentAsString(result)).contains("<td>" + user.name + "</td>");のようなViewの中身を意識した書き方をします。
非常に面倒ですし、厄介です。

ControllerとViewの役割を見直す

悩みながら改めて自分のコードを見直して気付いたのですが、Viewで使う変数は全てControllerから引数として渡そうとしていました。Viewのロジックをできるだけ減らして、ControllerとModelにロジックを書くような方針です。でもそうするとテストを書くのがつらい。
ということで、ロジックをControllerからViewに出来るだけ移すことにしました。Controllerをなるべくシンプルに、最低限のCRUD操作とエラーハンドリング、ページハンドリングに絞ることにしました。その結果テストが書きやすくなったと感じます。

Controllerのテスト

ようやくControllerのテストを書いてみます。よくある一覧を返すindexの処理です。 シンプルにするためにページングは行いません。

indexのメソッドを呼び出すには二つの方法があります。

  • callAction()
  • route()

callAction()

callAction()は、直接Controllerのメソッドを呼び出す方法です。
こんな感じで書きます。

Result result = callAction(
        controllers.routes.ref.UserController.index()
);

冗長なので、staticのimportをした方が良いと思います。

import static controllers.routes.ref.UserController.*;
     ・
     ・
     ・
Result result = callAction(UserController.index());

route()

route()は、擬似的にhttpリクエストを投げるので、conf/routesの定義も合わせてテストすることになります。ルーティング設計をしてからControllerを作ると思うので、ここはテストの分割にこだわる必要はないかなと思います。こんな感じで書きます。

Result result = route(fakeRequest(GET, "/users"));

シンプルでいいですね。

よくある200 OKで返すパターン

では、実際に一覧表示をするだけのindexメソッドのテストを書いてみます。 Inputとして、httpリクエストで”GET”で”/users”を投げたとき、Outputとして

を期待します。

import org.junit.*;

import static org.fest.assertions.api.Assertions.*;
import static play.test.Helpers.*;

import play.mvc.*;
import models.User;

public class UserControllerTest {
    @Before public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
    }

    @Test public void callIndex() {
        Result result = route(fakeRequest(GET, "/users"));

        assertThat(status(result)).isEqualTo(OK);
        assertThat(contentType(result)).isEqualTo("text/html");
        assertThat(charset(result)).isEqualTo("utf-8");
    }
}

あとは、indexを呼んだ結果としてUserのリストが画面に表示されればよいです。自動テストとしては、どのViewTemplateが呼び出されたかを確認できるといいのですが、今のところ直接Result型に入っているhtmlの内容をでテストする方法しか知りません。assertTemplate("index");みたいな書き方ができればいいのですが。。。

さて、htmlの内容をテストするには、contentAsString(result)を使います。この結果の中にUserのリスト情報が入っているかをチェックします。ただし、このテストをController側で書こうとすると、テストパターンが非常に多くなり、SessionとかCacheとか使い始めるとなおさら辛くなっていきます。そのため、私はControllerからViewに引数を渡す場合を除いて、contentAsString()によるテストは書かない方針にしています。

ダイレクトするパターン

play!でリダイレクトをすると、httpステータスコードは303 See Otherとなります。
例えば、POSTメソッドで処理された結果、リダイレクトでindex画面(/users)に返るとします。その場合は、

を期待します。

@Test public void callPost() {
    Result result = route(fakeRequest(POST, "/users/post”));

    assertThat(status(result)).isEqualTo(SEE_OTHER);
    assertThat(redirectLocation(result)).isEqualTo("/users");
}

ダイレクト先でcontentTypeとかcharsetをテストをしているので、リダイレクト元では必要ありません。(というか、そもそも書きようがない)

formデータ付きのPOSTリクエスト

faleRequest()にメソッドチェインで.withFormUrlEncodedBody(Map<String,String> params)を渡すとformのデータを送ったテストができます。Mapで渡すので、事前準備が少々冗長になります。

@Test public void callCreate() {
    Map<String, String> params = new HashMap<String,String>();
    params.put("name", "Alice");
    params.put("email", "alice@email.com");
    params.put("password", "password");

    Result result = route(
            fakeRequest(POST, "/users")
            .withFormUrlEncodedBody(params)
            );

    assertThat(status(result)).isEqualTo(SEE_OTHER);
    assertThat(redirectLocation(result)).isEqualTo("/users");
}

今回はPOSTの結果が正常終了してリダイレクトするパターンでしたが、POSTの内容に問題があった場合は、badRequest()で返すと思います。その場合は、httpステータスコードが400の”BAD_REQUEST”になっているかどうかをテストします。

assertThat(status(result)).isEqualTo(BAD_REQUEST);

form項目数の多い時にパターンテストをするのは大変なので、全部のパラメータをセットしたMapを返すメソッドを準備しておき、必要に応じて”remove”するようにしています。

callAction()でも以下のようにかけます。この後で書くsessionも同様です。

Result result = callAction(
     UserController.create(),
     fakeRequest().withFormUrlEncodedBody(params)
);

Session付きのリクエスト

fakeRequest()にメソッド・チェインで.withSession(“key”, “value”)をつなぐだけです。

Result result = route(
    fakeRequest(POST, "/users")
        .withFormUrlEncodedBody(params)
        .withSession("email", "alice@email.com")
);

これでControllerのおおよそのテストは書けるのではと思います。

Viewのテスト

Controllerと比べるとViewのテストは比較的シンプルです。Viewにロジックを書くため、DB内のデータ状態を整えるなどの事前準備が必要になる程度です。

package views;

import org.junit.Test;
import org.junit.Before;

import models.User;
import views.html.user.*;
import helper.UserHelper;

import static org.fest.assertions.Assertions.*;
import static play.test.Helpers.*;
import static play.data.Form.form;

public class UserViewTest {
    @Before public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
    }

    @Test public void indexRender() {
        Ebean.save((List) Yaml.load("testData/users.yml"));

        String htmlString
          = contentAsString(index.render());

        List<User> users = User.find.findList();
        User user = users.get(0);
        assertThat(htmlString).contains("<td>" + user.name + "</td>");
        assertThat(htmlString).contains("<td>" + user.email+ "</td>");
        assertThat(htmlString).contains("<a href=\"/users/" + user.id + "\">link</a></td>");
    }
}

contentAsString(index.render())とやるとView処理の出力(html)をString型で受けることができます。引数ありの場合はcontentAsString(show.render(user))のようにします。
あとはassertThatでcontains()startsWith()などを使ってテストしていきます。 DBの検証時の”状態”準備は、yamlで準備しておくと再利用可能でよいです。

その他

テスト書くときに読んだフレームワークのソースをあげておきます。

  • libexec/framework/src/play/src/main/scala/play/api/http/StandardValues.scala
    • いろんな定数が定義されているので、見ておくと便利。
  • libexec/framework/src/play/src/main/scala/play/api/Results.scala
    • assertion対象の状態が集約されているので、何が検証可能か確認できる。
  • libexec/framework/src/play-test/src/main/java/play/test/Helpers.java
    • controllerテストで使うメソッドや定数が詰まっているので目を通しておくと、読みやすいコードが書けると思う

おわりに

テストの書き方について整理してきましたが、WebSocketを使った場合とかはまだ書いたことないので万全ではないと思います。
なので@kara_dさんの「Play framework 2 徹底入門」などを読んで補完してもらえたらと思います。特に4章以降の実践編は、名前の通り複数人開発を想定した場合の注意点とかまで考慮されていて大変参考になりました。