FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

PlayでFunctional Test

テストの無いコードはレガシーコードだ!(挨拶

こんにちは、フリューの九岡です。

[前回(PlayでRESTful Webサービス)][1])に引き続き、今回はPlayのFunctional Testについて紹介します。

Functional Testって?

「Playのフレームワーク本体と、アプリケーション全体(Model-View-Controller)を通したテスト」です。

Playの公式ドキュメントでは以下のように説明されています。

A functional test is written using JUnit. In this kind of test you can test your application by accessing directly the controller objects.

(訳)

Functional Testを利用すると、Controllerを直接叩く事でアプリケーションをテストできます。

Play framework – Test your application

基本的なパターン

// FunctionalTestを継承してテストスイートとします。
// 実態は、JUnit4のテストケースにPlay独自のメソッドを追加したものになっています。
public class ApplicationTest extends FunctionalTest {
    // JUnit4のアノテーション
    // 各テストメソッドの直前に実行されます
    @Before
    public void setUp() {
        // Fixtureのロードなどを行う
        Fixtures.deleteAll();
        Fixtures.load("data.yml");
    }
    
    // JUnit4のアノテーション
    // これをつけたものがテストメソッドになります
    @Test
    public void testIndex() {
        // 特定のパスへのHTTP GETリクエストをシミュレートします
        Response response = GET("/index");
        // 上記レスポンスに対する各種assertをします
        assertIsOk(response);
    }
}

PlayのFunctionalTestでは、フレームワークとアプリケーションを通してテストするために、「HTTPリクエストをシミュレートして、レスポンスを検証」という方法を取ります。

コード例では以下の部分です。

Response response = GET("/index");
assertIsOk(response);

GETHTTP GETリクエスト、OkHTTPステータスコード200 OKを意味しています。

HTTPリクエストをシミュレートするメソッドは、GETの他にPOST、DELETE、PUTも用意されています。

レスポンスの検証についても、assertIsOkのようなステータスコードに対するものの他に、レスポンスヘッダやレスポンスボディを検証するメソッドが用意されています。その他、利用可能なメソッドについてはPlayのAPIリファレンスに記載されています。

FunctionalTest (Play! API)

テストクラスの構成

テストクラスは以下の2段構成で、普通のJUnitテストケースと同じです。

  1. テスト環境の準備
  2. 各テストメソッドの実行

テスト環境の準備

public class ApplicationTest extends FunctionalTest {
    static File preparedFile;
    Photo preparedPhoto;

    @BeforeClass
    public static void prepareFile() {
        preparedFile = Play.getFile("public/images/favicon.png");
    }

    @Before
    public void setUp() throws FileNotFoundException {
        Fixtures.deleteAll();
        Fixtures.load("data.yml");
        preparePhoto();
    }

    private void preparePhoto() throws FileNotFoundException {
        preparedPhoto = new Photo();
        preparedPhoto.title = "example title";
        preparedPhoto.data = new Blob();
        preparedPhoto.data.set(new FileInputStream(preparedFile), "image/png");
        preparedPhoto.save();
    }
// 略

Beforeアノテーションをつけたメソッドは各テストメソッドの実行前、BeforeClassはテストクラスの実行前にそれぞれ実行されます。(この二つ以外にもJUnit4が提供しているアノテーションは利用できます。詳しくはJUnit4のドキュメントを参照してください。)

通常、GET以外の操作はデータの追加・削除・更新など何らかの副作用を伴うはずです。そのような場合に、テストメソッドの実行順序でテスト結果が変わってしまうことのないように、BeforeやBeforeClassのタイミングで初期化を行うことをオススメします。

ここでは、テストクラスの実行前にテスト中にずっと使い回す一時ファイルの作成を行っています。また、各テストメソッドの実行前には、

Fixtures.deleteAll();

でデータベースの内容を全てクリアした後、

Fixtures.load("data.yml");

でdata.ymlの内容をDBにロード、さらに各テストケースの事前条件としてPhotoを1件作成してDBに保存しています。

なお、テスト時にはapplication.confでテスト用として指定したDBが利用されます。play newでプロジェクトのひな形を生成したあと、特に変更していなければ、

%test.db.url=jdbc:h2:mem:play;MODE=MYSQL;LOCK_MODE=0

のようにインメモリーデータベースになっているはずです。

テストメソッドの例

前回実装した[PlayでRESTful Webサービスをつくる][1]をネタに、いくつかテストメソッドを書いてみます。

リソースの参照(GET)

テキストの場合

@Test
    public void testIndexHtml() {
        Response response = GET("/photos");
        assertIsOk(response);
        assertContentType("text/html", response);
        assertCharset("utf-8", response);
    }

    @Test
    public void testIndexXml() {
        Response response = GET("/photos.xml");
        assertIsOk(response);
        assertContentType("text/xml; charset=utf-8", response);
    }

    @Test
    public void testIndexJson() {
        Response response = GET("/photos.json");
        assertIsOk(response);
        assertContentType("application/json; charset=utf-8", response);
    }

上から順に、HTML・XMLJSONで/photosをリクエストしたときのレスポンスをテストしています。

assertContentTypeでレスポンスのContent-Type、assertCharset文字コードが期待通りであることを検証しています。

また、assertIsOkでステータスが”200 OK”であることを検証しています。レスポンスに応じたContent-Type、Charsetをセットするのは忘れがちなので注意したいところです。

バイナリの場合

@Test
    public void testData() throws Exception {
        Response response = GET("/photos/" + preparedPhoto.getId() + "/data");
        assertEquals(preparedFile.length(), response.out.toByteArray().length);
        assertContentType("image/png", response);
        assertStatus(200, response);
    }

preparePhoto()で用意しておいたPhotoの画像ファイルを取得するリクエストを送信して、ステータスが200 OK、Content-Typeがimage/pngで、かつ返却されたデータの長さが正しい事を検証しています。

(データの中身を比較しても良かったのですが、ここでは長さで必要十分と判断しました)

リソースの追加(POST)

@Test
    public void testUpload() throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("photo.title", "title");

        Map<String, File> files = new HashMap<String, File>();
        File file = Play.getFile("public/images/favicon.png");
        files.put("photo.data", file);

        Response response = POST("/photos.json", params, files);

        // レスポンスの検証
        JsonObject responseJson = getJsonObject(response);
        Long actualPhotoId = Long.parseLong(responseJson.getAsJsonObject("photo").getAsJsonPrimitive("id").getAsString());

        assertContentType("application/json; charset=utf-8", response);
        assertStatus(200, response);

        // 副作用の検証
        Photo uploadedPhoto = Photo.findById(actualPhotoId);
        assertNotNull(uploadedPhoto);
        assertEquals(file, uploadedPhoto.data.getFile());
        assertEquals("http://localhost/photos/" + uploadedPhoto.getId() + "/data", uploadedPhoto.dataUrl);
    }

ファイルを実際にアップロードするリクエストを送信して、レスポンスが正しい事、またアップロードしたファイルの情報がDBに保存されていることを検証しています。

レスポンス(JSON形式)の検証のため、Playに標準添付されているGson(Google製のJSONライブラリ)を利用しています。

リソースの更新(PUT)

@Test
    public void testUpdate() {
        String newTitle = "modified title";
        Response response = PUT("/photos/" + preparedPhoto.id, "application/x-www-form-urlencoded", "title=" + newTitle);
        assertStatus(200, response);
    }

Photoのtitleを変更するリクエストを送信して、200 OKが返却されることを確認しています。

リソースの削除(DELETE)

@Test
    public void testDelete() {
        Response response = DELETE("/photos/" + preparedPhoto.id);
        assertStatus(200, response);
    }

指定したIDのPhotoを削除するリクエストを送信して、200 OKが返却されることを確認しています。

総括

  • PlayでFunctional Testを書きました。
  • Functional Testでは、コントローラに対してGET/POST/PUT/DELETEなどのリクエストを送信して、ステータスコードやレスポンスボディなどが期待通りかを検証することでテストを行います。

Playにはこれ以外にUnit TestとIntegration Testも用意されていますが、投資対効果では個人的にこのFunctional Testが一番だと思います。

また、今回も

ソースコードはGitHubで公開していますので、参考にしてください。

この機会にぜひテストを書いてみましょう!