テストの無いコードはレガシーコードだ!(挨拶
こんにちは、フリューの九岡です。
[前回(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を直接叩く事でアプリケーションをテストできます。
基本的なパターン
// 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);
GET
はHTTP GET
リクエスト、Ok
はHTTPステータスコードの200 OK
を意味しています。
HTTPリクエストをシミュレートするメソッドは、GETの他にPOST、DELETE、PUTも用意されています。
レスポンスの検証についても、assertIsOkのようなステータスコードに対するものの他に、レスポンスヘッダやレスポンスボディを検証するメソッドが用意されています。その他、利用可能なメソッドについてはPlayのAPIリファレンスに記載されています。
テストクラスの構成
テストクラスは以下の2段構成で、普通のJUnitテストケースと同じです。
- テスト環境の準備
- 各テストメソッドの実行
テスト環境の準備
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・XML・JSONで/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で公開していますので、参考にしてください。
この機会にぜひテストを書いてみましょう!