id:KenichiroMurata(@muraken720 )です。
皆さん、Vert.x 使っていますか?
私は先日(11/9)開催された JJUG CCC 2013 Fallに参加し、「Over the Node.js. An Introduction to Vert.x」というタイトルで発表させて頂きました。
内容はこの「Vert.x がいいね!」の第1回〜第3回で紹介した内容をベースとして、現在の最新バージョンであるVert.x 2.1Mの新機能の紹介、node.jsで開発したfront-endとvert.xアプリケーションをeventbusでつなぐためのnpmモジュール、vertx-eventbus-clientなどを紹介しています。スライドは以下にありますので、興味のある方はぜひご覧下さい。
さて、少し間が空いてしましいましたが、第4回目となる今回は「テストする」と題して、Vert.xのテストのやり方について書きます。
目次は以下の通りです。
- Vert.x's Tests
- BusMod Common Practices
- Integration Tests
- Gradle Jacoco Plugin (Code coverage)
- まとめ
1. Vert.x's Tests
Vert.xのテストについては公式ドキュメントの開発ガイドとGradle Project Templateを見ると情報があります。
Vert.xのテストにはunitテストとintegrationテストという2種類のテストがあります。
unitテスト
unitテストは特別なものではなく、JUnitを使った通常のunitテストです。Vert.xアプリケーションはVerticleを作成し、そのVerticleをVert.x Instance上で動作させるのですが、このunitテストは、ただ単純に対象のクラスをテストするだけです。
integrationテスト
では、Vert.x 上でテスト対象のVerticleを実際に動作させたテストをしたい場合はどのようにすれば良いでしょうか?例えばeventbusを使ったメッセージによってRequestを受け、なにか処理をしてからResponseを返すVerticleを作ってテストしたい場合です。
Vert.xではintegrationテストという位置づけのテストを定義しており、このintgrationテストがテスト対象のVerticleをVert.x上で動作させてテストを行うものです。Vert.xではその実現のために testtoolsを提供しており、このtesttoolsの中に、JUnitを拡張したTestVerticleというテストクラスを用意しています。
このTestVerticleを継承したテストクラスを作成することで、Vert.xのAPIが使えるようになるため、テスト対象のVerticleをVert.x上で動かしたテストを簡単に作成することができます。
2. BusMod Common Practices
それではここから、eventbusを使ったモジュールを作成し、そのintegrationテストを書いていきます。
Vert.xでモジュールを作成する場合、基本のインタフェースはeventbusによるReq/Res、またはPub/Subメッセージになります。eventbusをインタフェースとしたモジュールのことをVert.xでは慣例的にBusModと呼んでいるようです。
このBusModを作成するにあたり、公式モジュールやその他モジュールをいくつか調べると、メッセージインタフェースの共通プラクティスと言えるものがあるようですので、今回のモジュールもそれに従いたいと思います。
2.1. メッセージにはJSONメッセージを使う
Vert.xの特長としてPolyglotがあります。Java, Groovy, JavaScript, Ruby, Python, Scala, Clojureなど様々なJVM上で動作する言語を使うことができます。言語間の差分を意識せずにメッセージをやり取りするには、メッセージにJSONを使うことが推奨されいます。
2.2. Req/Resのメッセージにはstatusプロパティを使う
Req/Res型のメッセージを使う場合、Requestに対する結果が正常だったのか、異常だったのかを知る必要があります。この正常/異常の処理結果を知るためのフィールドとして、JSONメッセージにstatusプロパティを設けて、正常の場合は"ok"、異常だった場合は"error"を返すようにします。
- Responseのstatusプロパティでok/errorを表現する
{"status":"ok"}
2.3. statusがerrorの場合はmessageプロパティでエラーメッセージを返す
上記のstatusが"error"だった場合、何がエラーだったのか原因を知るためにエラーメッセージが必要です。statusが"error"であった場合は、messageプロパティを設けて、そのエラーメッセージを設定して返すようにします。
- Responseのstatusがerror時にはmessageプロパティを使う
{"status":"error","message":"unknown action."}
2.4. メッセージの種類を分けるにはactionプロパティを使う
eventbusによってメッセージを受けるためには、eventbusに対して受信するためにドメイン(イベントパス)を指定してHandlerを登録します。1つのドメインに対するメッセージの中で、メッセージの種別によって処理を切り替えるような実装をする際には、actionプロパティを設けて、その値によって、例えば"add"、"update"、"delete"、"search"などを使います。
- Requestのactionプロパティでメッセージの種別を分ける
{"action":"add","key":"name","value":"@muraken720"}
3. Integration Tests
それでは実際にBusModを作成し、そのintegrationテストを書いてみましょう。まずは、テスト対象となるBusModです。
BusModVerticle
public class BusModVerticle extends Verticle { public void start() { vertx.eventBus().registerHandler("muraken720.vertx.mod.testexample", new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> message) { ConcurrentMap<String, String> map = vertx.sharedData().getMap( "muraken720.testexample"); JsonObject json = message.body(); if ("add".equals(json.getString("action"))) { String key = json.getString("key"); String value = json.getString("value"); map.put(key, value); } else { message.reply(new JsonObject().putString("status", "error") .putString("message", "unknown action.")); } message.reply(new JsonObject().putString("status", "ok")); } }); } }
このBusModの仕様を簡単に列挙します。
- イベントの受信ドメインは「muraken720.vertx.mod.testexample」とする
- JSONメッセージを使う
- Requestメッセージにて、action="add" を指定した場合、指定されたkeyとvalueによってSharedDataのmapにデータを登録する
- 正常に処理した場合は status="ok" のResponseメッセージを返す
- Requestメッセージにて、action="add"以外の値が指定された場合、status="error" のResponseメッセージを返す
BusModVerticleTest
上記のBusModをテストするためのテストクラス、BusModVerticleTestを作成します。
モジュールのintegrationテストをするには、TestVerticleを継承します。また、startメソッドにて初期化が必要になります。
public class BusModVerticleTest extends TestVerticle { @Override public void start() { // Make sure we call initialize() - this sets up the assert stuff so assert // functionality works correctly initialize(); // Deploy the module - the System property `vertx.modulename` will contain // the name of the module so you // don't have to hardecode it in your tests container.deployModule(System.getProperty("vertx.modulename"), new AsyncResultHandler<String>() { @Override public void handle(AsyncResult<String> asyncResult) { // Deployment is asynchronous and this this handler will be called // when it's complete (or failed) if (asyncResult.failed()) { container.logger().error(asyncResult.cause()); } assertTrue(asyncResult.succeeded()); assertNotNull("deploymentID should not be null", asyncResult.result()); // If deployed correctly then start the tests! startTests(); } }); } }
ここはGradle Project Templateの実装をそのまま利用します。やっていることは、モジュールをVert.xのcontainerにdeployし、deployが完了したらテストを開始するというものです。
BusModVerticleTest.testAddAction
それでは次に正常系となる1つ目のテストケース、「testAddAction」を作成します。
@Test public void testAddAction() { container.logger().info("in testAddAction()"); JsonObject request = new JsonObject().putString("action", "add") .putString("key", "name").putString("value", "@muraken720"); container.logger().info("request message: " + request); vertx.eventBus().send("muraken720.vertx.mod.testexample", request, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> reply) { JsonObject json = reply.body(); container.logger().info("response message: " + json); assertEquals("ok", json.getString("status")); ConcurrentMap<String, String> map = vertx.sharedData().getMap( "muraken720.testexample"); assertEquals("@muraken720", map.get("name")); testComplete(); } }); }
Request用のメッセージを生成し、eventbusに対してメッセージを送信します。Responseメッセージを受信したら、statusが"ok"であること、さらに、SharedDataのmapを取得し、要求したkeyとvalueが保存されていることを確認します。
TestVerticleでテストケースを書く際には1つ決まりごとがあります。それはテストケースの最後に「testComplete()」を呼び出してテストケースの終了を通知するというものです。これは非同期APIによって処理が行われるため、テストを終わりを通知しなければ完了したことが分からないためです。
BusModVerticleTest.testUnknownAction
それでは異常系となる2つ目のテストケース、「testUnknownAction」を作成します。
@Test public void testUnknownAction() { container.logger().info("in testUnknownAction()"); JsonObject request = new JsonObject().putString("action", "unknown") .putString("key", "name").putString("value", "@muraken720"); container.logger().info("request message: " + request); vertx.eventBus().send("muraken720.vertx.mod.testexample", request, new Handler<Message<JsonObject>>() { @Override public void handle(Message<JsonObject> reply) { JsonObject json = reply.body(); container.logger().info("response message: " + json); assertEquals("error", json.getString("status")); assertEquals("unknown action.", json.getString("message")); testComplete(); } }); }
TestVerticleによるテストケースはVert.xのAPIを使う以外は通常のJUnitにと同じですから、特に問題なく書けると思います。
4. Gradle Jacoco Plugin (Code coverage)
今回のテストプロジェクトはgithubのこちらにありますので、全体を見たい方は参照してください。
このプロジェクトには、gradleのjacoco pluginを設定し、カバレッジを計測するできるようにしていますので、テストのカバレッジレポートを見ることができます。
ken@vertx-test $ ./gradlew test
上記のテスト実行で、build/reports/tests/index.htmlにテストレポートが、
ken@vertx-test $ ./gradlew jacocoTestReport
上記のタスク実行で、build/reports/jacoco/index.htmlにカバレッジレポートが生成されます。
テストをする上で、カバレッジを知りたいことがありますが、gradleのjacoco pluginを使えば簡単に確認できます。
5 まとめ
いかかでしょうか?Vert.xでアプリケーションを開発する上で一番開発が多いのがこのBusModです。非常に簡単なテストの例ではありましたが、そのテストをintegrationテストを使うことで簡単に実施できることが分かったと思います。ノンブロッキングで非同期なアプリケーションを開発する場合でも、このようにテストが書ければ、開発が捗りますね。
次回は?
Vert.xのテストの続きかコールバックヘルにどう対応するか辺りを取り上げたいと思います。
See also
- Vert.x がいいね!(第1回:入門する) - Taste of Tech Topics
- Vert.x がいいね!(第2回:開発環境を構築する) - Taste of Tech Topics
- Vert.x がいいね!(第3回:Event LoopsとVerticle Instances) - Taste of Tech Topics
- RxJavaを使ってCallback Hellから脱出する( Vert.x がいいね!第5回 ) - Taste of Tech Topics
- RxJavaを使ってCallback Hellから脱出する( Java8 ラムダ編 ) - Taste of Tech Topics
Acroquest Technologyでは、キャリア採用を行っています。
- 日頃勉強している成果を、Hadoop、Storm、NoSQL、HTML5/CSS3/JavaScriptといった最新の技術を使ったプロジェクトで発揮したい。
- 社会貢献性の高いプロジェクトに提案からリリースまで携わりたい。
- 書籍・雑誌等の執筆や対外的な勉強会の開催を通した技術の発信や、社内勉強会での技術情報共有により、技術的に成長したい。
- OSSの開発に携わりたい。
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
キャリア採用ページ
11/24(日) DODA転職フェアに参加します!ぜひブースにお立ち寄りください。