はじめに
例えば、UI ウィジェットを生成して何かしたり、サービスの API を呼び出して何かしたりするコードは、テストが困難です。DOM が変更されたら正しいテストにならない可能性があるし、API 呼び出しで環境が破壊されたら困りますからね。
そこでモックの出番。Closure Library はモックを作成する機能を提供しています。
モックを作成してみます
goog.testing.MockClassFactory を使えば、任意のクラスのモックを作成できます。
モックを使ったテストの流れは次の通り。
- ファクトリ作成
- モック作成
- モックの振る舞い指定
- モックの振る舞い再生
- テスト
- モックが正しく呼び出されたか検証
検証では、振る舞いを定義した順番通りに、モックが呼び出されたかもチェックできます。
<html> <head> <title>MockSample</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.MockClassFactory"); goog.require("goog.ui.MenuItem"); </script> <script type="text/javascript"> // モックを作成するためのファクトリ var factory = new goog.testing.MockClassFactory(); function tearDown() { // 作成したモックを破棄 factory.reset(); } // メニュー項目を作成する関数 function buildMenuItem(id, text) { var item = new goog.ui.MenuItem(text); item.setId(id); item.setModel({ id: id, name: text }); return item; } function testBuildMenuItem() { // メソッドの呼び出し順を検証するモックを作成 var mock = factory.getStrictMockClass( goog.ui, goog.ui.MenuItem, "新規作成"); // モックの振る舞いを記録 mock.setId("NEW_PROJECT"); mock.setModel({id:"NEW_PROJECT",name:"新規作成"}); // モックの振る舞いを再生 mock.$replay(); var item = buildMenuItem("NEW_PROJECT", "新規作成"); // 記録した振る舞い通り呼び出されたか検証 mock.$verify(); } </script> </body> </html>
検証時に呼び出し順をチェックしないモックも作れます。詳しくはドキュメントを読むべし。
クラスメソッドのモックが作れます!
goog.testing.MethodMock クラスを使えばね。goog.string.buildString みたいなクラスメソッドのモックが作れます。
<html> <head> <title>MethodMockSample</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.MethodMock"); goog.require("goog.string"); </script> <script type="text/javascript"> // TreeNode に表示するコンテンツを作成する関数 function createTreeNodeContent(html) { return goog.string.buildString( "<span>", html, "</span>"); } function testCreateTreeNodeContent() { var mock = new goog.testing.MethodMock(goog.string, "buildString"); try { // モックの振る舞いを記述 mock("<span>", "sample", "</span>").$returns("<span>sample</span>"); // モックの振る舞いを再生 mock.$replay(); // テスト var content = createTreeNodeContent("sample"); assertEquals("<span>sample</span>", content); // モックが正しく呼ばれたか検証 mock.$verify(); } finally { // 後始末 mock.$tearDown(); } } </script> </body> </html>
グローバル関数のモックも作れます!
goog.testing.GlobalFunctionMock クラスを使えば、alert や promt や confirm などの、グローバルな関数のモックが作れます。promt や confirm を使ってる機能の自動テストが書けるのは助かるなぁ。
<html> <head> <title>GlobalFunctionMockSample</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.GlobalFunctionMock"); </script> <script type="text/javascript"> function newProject() { return prompt("新しいプロジェクト"); } function testNewProject() { // グローバル関数 prompt のモックを作成 new goog.testing.GlobalFunctionMock("prompt"); try { // prompt の振る舞いを記録 prompt("新しいプロジェクト").$returns("sample"); // prompt の振る舞いを再生 prompt.$replay(); assertEquals("sample", newProject()); prompt.$verify(); } finally { prompt.$tearDown(); } } </script> </body> </html>
goog.net.XhrIo のスタブまで提供されています!
Closure Library を使った開発でサービスの API を呼び出すとき、goog.net.XhrIo クラスを使いますが、この XhrIo クラスと同じインタフェースを持つスタブが提供されています。goog.testing.net.XhrIo がそれ。
<html> <head> <title>StubSample</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.TestQueue"); goog.require("goog.testing.net.XhrIo"); goog.require("goog.events"); goog.require("goog.net.EventType"); </script> <script type="text/javascript"> var projects = null; // プロジェクトの一覧を取得する関数 function getProjectList(xhrio) { goog.events.listen( xhrio, goog.net.EventType.SUCCESS, function(e) { projects = e.target.getResponseJson(); }); xhrio.send("/project/"); } function testGetProjectList() { // TestQueue は XhrIo に渡された値を保存するためのもの var queue = new goog.testing.TestQueue(); // スタブ作成 var xhrio = new goog.testing.net.XhrIo(queue); // テスト対象の関数呼び出し getProjectList(xhrio); // XhrIoが正しく呼び出されたかチェック assertFalse("リクエストが発行されていません", queue.isEmpty()); var req = queue.dequeue(); assertEquals("URLが不正です", req[1], "/project/"); // サービスのレスポンスをシミュレートする xhrio.simulateResponse(200, '[{"name":"project0"},{"name":"project1"}]', {}); // サービスが返したデータを正しく処理できたかチェック assertNotNullNorUndefined("レスポンスが不正です", projects); assertEquals("レスポンスが不正です", "project0", projects[0].name); } </script> </body> </html>
実際の開発だと、XhrIo を生成してメソッドに渡す、なんてことはしないですけど。関数の中で XhrIo のコンストラクタを呼び出して作るのがほとんど。XhrIo のコンストラクタをモックにして、XhrIo スタブにすり替えると良さそう。
まとめ
Closure Library のモック機能は、差し替えられないものはないんじゃないか、ってくらい強力。実際、モックを使えば、ほとんどの機能の単体テストが書けました。
HTML + JavaScript での RIA 開発だと、どうしても JavaScript の行数が多くなります。サーバー側のコードより多くなる、なんてことも…。モックを使って細かい粒度でテストし、品質を高めていかないとね。