はじめに
UI フレームワークとかすっとばして、いきなりテストです。
Closure Library 使って Web アプリを作るということは、それなりの規模の RIA を作るということ。ちょっとしたやつなら jQuery でいいですからね。
それなりの規模の RIA の品質を高めるには、もはや当たり前かもしれませんが、テストが重要。早い段階でテスト機能を使いこなせるようになっておくべきです。
テストの記述
Closure Library は JsUnit を使ったテストをサポートしています。JsUnit の使い方を簡単に説明するなら次の通り。
- setUp という名前の関数を作り、テストの準備を行う処理を書く
- tearDown という名前の関数を作り、テストの後始末を行う処理を書く
- test で始まる名前のグローバル関数でテストを書く
JUnit や NUnit を使ってテストを書いた経験があるなら、スンナリ使えるはずです。
データソースの使い方を覚えたばかりなので、データソースを使ってテストのサンプルを書いてみました。
<html> <head> <title>TestSample</title> </head> <body> <script type="text/javascript" src="closure-library/closure/goog/base.js"></script> <script type="text/javascript"> // Closure Library は JsUnit をサポート goog.require("goog.testing.jsunit"); // テストで使うクラスを読み込む goog.require("goog.ds.JsDataSource"); goog.require("goog.ds.DataManager"); goog.require("goog.ds.Expr"); </script> <script type="text/javascript"> var testData = null; // テストの準備 function setUp() { testData = [ { key: "key0", name: "foo0" }, { key: "key1", name: "foo1" }, { key: "key2", name: "foo2" } ]; var ds = new goog.ds.JsDataSource(testData, "test"); var dm = goog.ds.DataManager.getInstance(); dm.addDataSource(ds); } // テストの後始末 function tearDown() { goog.ds.DataManager.clearInstance(); } // ノードを削除するテスト function testDeleteNode() { var expr = goog.ds.Expr.create("$test"); var node = expr.getNode(); node.setChildNode("[0]", null); assertEquals("ノードの削除", 2, testData.length); } // ノードの値を更新するテスト function testUpdateNode() { var expr = goog.ds.Expr.create("$test/[0]/name"); var node = expr.getNode(); node.set("bar0"); assertEquals("ノードの値更新", "bar0", testData[0].name); } </script> </body> </html>
Assert 関数はたくさんあるので、ドキュメントを見てください。
DOM イベントのシミュレーション
JavaScript を使って書くのは Web アプリの UI 部分なので、ボタンを押されたときの処理やマウスでダブルクリックされたときの処理を書くことは多いですよね。この部分をテストするには、DOM イベントのシミュレーションが必要。
ちゃんと、Closure Library は DOM イベントのシミュレーションをサポートしています。ただ、Closure Library が提供する UI ウィジェットを使ってると、DOM イベントを直接さわる事は少ないですけど。DOM イベントで何かするクラス、たとえばウィジェットのテストで使うことになりそう。
使い方は簡単。goog.testing.events にクリックやダブルクリックなどをシミュレートする関数があるので、それを呼び出すだけ。
<!--event_test.html--> <html> <head> <title>EventTestSample</title> </head> <body> <input id="greet" type="button" value="Greet"/> <script type="text/javascript" src="closure-library/closure/goog/base.js"></script> <script type="text/javascript"> goog.require("goog.testing.jsunit"); goog.require("goog.testing.events"); goog.require("goog.dom"); goog.require("goog.events"); </script> <script type="text/javascript"> var result = null; // ボタンをクリックしたら挨拶する function onGreet(e) { result = "Hello"; } goog.events.listen( goog.dom.getElement("greet"), goog.events.EventType.CLICK, onGreet); // テストの後始末 function tearDown() { result = ""; } // ボタンをクリックしたときのテスト function testGreet() { // DOM の click イベントを発生させる goog.testing.events.fireClickEvent(goog.dom.getElement("greet")); assertEquals("挨拶テスト", "Hello", result); } </script> </body> </html>
非同期処理のテスト
フル Ajax な UI を作るなら、サーバーからデータを取得するのも当然非同期。この場合のテストって、非同期処理が完了するまでテストを終了しない、みたいなコードを書く必要があるから面倒です。
Closure Library には、その部分を少しだけ楽に書ける機能が提供されています。goog.testing.AsyncTestCase クラスがそう。サンプルは下の通り。
<!--async_test.html--> <html> <head> <title>AsyncTestSample</title> </head> <body> <script type="text/javascript" src="closure-library/closure/goog/base.js"></script> <script type="text/javascript"> goog.require("goog.testing.jsunit"); goog.require("goog.testing.AsyncTestCase"); </script> <script type="text/javascript"> // 非同期処理のテストの準備 // 非同期処理のテストに必要な情報がページに読み込まれる var asyncTestCase = new goog.testing.AsyncTestCase.createAndInstall(); // テスト対象の非同期メソッド function asyncGreet(name, opt_callback) { setInterval(function(){ var msg = "Hello, " + name + "."; if (opt_callback) { opt_callback(msg); } }, 100); // 100ms待つ }; // 非同期メソッドのテスト function testAsyncGreet() { // continueTesting が呼ばれるまでテストを終了しない asyncTestCase.waitForAsync("Greet"); asyncGreet("Ichiro", function(msg) { assertEquals("非同期挨拶テスト", "Hello, Ichiro.", msg); // テスト続行 asyncTestCase.continueTesting(); }); } </script> </body> </html>
テストランナー
Google Closure には、複数のテストをまとめて走らせることができるテストランナーがあります。JUnit や NUnit を使ったことがある人にはおなじみ、みんな大好きグリーンバーがあるヤツです。こんなの。
ウィジェットとして提供されているので、専用の HTML ファイルを用意します。テストランナーで実行するテストを指定する方法は、テストが書かれた HTML ファイルのパスを渡すしかありません。
テスト毎にテストランナー用 HTML ファイルを作るのは面倒なので、all_tests.html みたいなファイルを1個用意し、全てのテスト用 HTML を走らせるようにするのがいいです。Closure Library もそうなっています。
<!--all_tests.html--> <html> <head> <title>TestSample</title> <link rel="stylesheet" type="text/css" href="closure-library/closure/goog/css/multitestrunner.css"> </head> <body> <!--ここにテストランナーを表示--> <div id="runner"></div> <script type="text/javascript" src="closure-library/closure/goog/base.js"></script> <script type="text/javascript"> goog.require("goog.testing.MultiTestRunner"); goog.require("goog.dom"); </script> <script type="text/javascript"> var testRunner = new goog.testing.MultiTestRunner(); // テストを登録。 // JsUnit テストを記述した HTML ファイルのパスを // 配列で渡す。 testRunner.addTests([ "sample_test.html", "event_test.html", "async_test.html" ]); // テストランナーを描画 testRunner.render(goog.dom.getElement("runner")); </script> </body> </html>
まとめ
Closure Library のテスト機能は JsUnit ベースで使い方が簡単な上に、DOM イベントのシミュレーションと非同期処理のテストをサポートしているのが強力。まだ触ってないけど、モックも提供されているみたいなので、組み合わせれば「出来ないテストなんてないんじゃないか」ってくらいです。