読者です 読者をやめる 読者になる 読者になる

Closure Library 超入門 〜モック編〜

はじめに

例えば、UI ウィジェットを生成して何かしたり、サービスの API を呼び出して何かしたりするコードは、テストが困難です。DOM が変更されたら正しいテストにならない可能性があるし、API 呼び出しで環境が破壊されたら困りますからね。

そこでモックの出番。Closure Library はモックを作成する機能を提供しています。

モックを作成してみます

goog.testing.MockClassFactory を使えば、任意のクラスのモックを作成できます。

モックを使ったテストの流れは次の通り。

  1. ファクトリ作成
  2. モック作成
  3. モックの振る舞い指定
  4. モックの振る舞い再生
  5. テスト
  6. モックが正しく呼び出されたか検証

検証では、振る舞いを定義した順番通りに、モックが呼び出されたかもチェックできます。

<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 の行数が多くなります。サーバー側のコードより多くなる、なんてことも…。モックを使って細かい粒度でテストし、品質を高めていかないとね。