SiNBLOG

140文字に入らないことを、極稀に書くBlog

Slim3でClosure LibraryのUnitTestを使ってみた・・・が。

UnitTestが無いコードは、レガシーコード!と言われるぐらい昨今UnitTestは重要なものとなっています。
僕もフレームワークに求めるのはテストのしやすさなのかも!と思ってます。

ClosureLibraryにもUnitTestをするために機能があります。
JsUnitを使ったテストをサポートしており、ブラウザ上で実行できるようになっています。

こちらのBlog記事に詳しく書いて下さってます。
Closure Library 超入門 〜テスト編〜 - present


さて、それをSlim3で使う場合、どうするかですが、以下のようにしてみました。


war
| all_test.html //全テスト実行用のhtml

+--closurelibrary
|
+--ajaxsample
+--files
| scripts.js
| stylesheet.css
+--gss
| stylesheet.gss
+--scripts
| app.js
| deps.js
| ajaxsample_test.html //これがUnitTestのhtml
| index.html
| depswriter.bat


ただ、Production環境にはUnitTest用のhtmlをUploadしたくないので、
appengine-web.xmlのstatic-filesとresource-filesに
を設定します。


















これで、Production環境ではUnitTest用のhtmlが、404になるようになりました。


それでは、UnitTest用のhtmlの中身です。
all_test.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>AllTest</title>
        <link rel="stylesheet" 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();

            var testPaths = ["/closurelibrary/ajaxsample/ajaxsample_test.html"];
            testRunner.addTests(testPaths);

            testRunner.render(goog.dom.getElement("runner"));
        </script>
    </body>
</html>

サンプルから持ってきたもの、そのままです。
一番、上に張ったBlog記事から持ってきたものなので、そちらをご覧ください。

動くものが見たい方もいるかと思うので、Production環境でも動くようにしてみました。
中身は薄いですが・・・。
ログイン - Google アカウント

ajaxsample.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Ajax Sample Test</title>
        <script src="/closure-library/closure/goog/base.js">
        </script>
        <script>
            goog.require('goog.testing.AsyncTestCase');
            goog.require('goog.testing.jsunit');
        </script>
        <script src="deps.js">
        </script>
    </head>
    <body>
        <div id="xhr-indicator">
            アクセス中
        </div>
        <script>
            goog.require('ajaxsample.App');
        </script>
        <script>
            var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();

            function testXhrIo(){
                ajaxsample.App.getInstance().sendHoge();
            }
        </script>
    </body>
</html>

そして、こちらが問題でして・・・。
タイトルの"・・・が"の部分です。
どう書けば、自分がやりたいUnitTestができるのか分からなかったのですね・・・。
とりあえず、ajaxsample.Appのメソッドを呼ぶことはできました。
しかし、今回テストしようとしたのは、Ajax部分です。
できれば、Serverとのやり取りの部分をMock化して、やりたかったのですが、
どうすれば良いのか分かりませんでした。

Google API Expertが解説する Closure Libraryプログラミングガイド

Google API Expertが解説する Closure Libraryプログラミングガイド

ClosureLibrary本にもテストフレームワークのことは触れられているのですが、
僕の応用力が足りないため、なかなか思いつかず・・・。

テストしようとしているAjaxのメソッドはtinywordと同じくXhrManagerを利用しています。
本にはXhrManagerのsend()メソッドをMockにするという手もあると書いてあるのですが、
どうやって?というレベルで止まってます・・・。


また、何故か置いてある以下の部分。

<div id="xhr-indicator">
    アクセス中
</div>

Ajax通信時にインジケーターを表示しているのですが、中でこのdivを取得しています。
そのため、これが無いとErrorになるので、置いているのですが、これもどうすれば良いのか・・・。
まだまだ、ClosureLibraryのUnitTestを扱うためには修行が必要そうです。


こちらもProduction環境で動くようにしてみました。
ログイン - Google アカウント
上にアクセス中とか出ているのが、インジケーターです。
妙にそれっぽく出ていますが、出そうとして出しているわけではなく、上記の理由により出ています。


以下がテストしようとしているソースです。
index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Ajax Sample</title>
        <link rel="stylesheet" href="/tinyword/files/stylesheet.css">
        <script src="/closure-library/closure/goog/base.js">
        </script>
        <script src="deps.js">
        </script>
        <!--
        <script src="files/scripts.js">
        </script>
        -->
    </head>
    <body>
        <h1 id="title">AjaxSample</h1>
        <div id="xhr-indicator">
            アクセス中
        </div>
        <!-- このdiv内にアプリケーションUIを表示する -->
        <div id="main">
        </div>
        <script>
        goog.require('ajaxsample.App');
        </script>
    </body>
</html>


app.js

goog.provide('ajaxsample.App');
goog.require('goog.events.EventHandler');
goog.require('goog.ds.DataManager');
goog.require('goog.ds.JsDataSource');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XhrManager');
goog.require('goog.object');
goog.require('goog.fx.dom');
goog.require('goog.History');
goog.require('goog.Uri');

goog.scope(function(){
    var dom = goog.dom;
    var events = goog.events;
    var EventType = goog.events.EventType;
    var DataManager = goog.ds.DataManager;
    var JsDataSource = goog.ds.JsDataSource;

    /** @constructor */
    ajaxsample.App = function(){
        this.initialize_();
    };
    goog.addSingletonGetter(ajaxsample.App);

    // アプリケーションを初期化
    ajaxsample.App.prototype.initialize_ = function(){
        // イベントハンドラを管理するためのEventHandlerを生成
        this.eventHandler_ = new events.EventHandler(this);

        this.xhrManager_ = new goog.net.XhrManager();
        this.nextXhrId_ = 1;

        //インジケータ設定
        var indicator = dom.getElement('xhr-indicator');
        this.fadeInIndicator_ = new goog.fx.dom.FadeInAndShow(indicator, 10);
        this.fadeOutIndicator_ = new goog.fx.dom.FadeOutAndHide(indicator, 1000);
        this.fadeOutIndicator_.play(true);

        this.eventHandler_.listen(this.xhrManager_, goog.net.EventType.READY, this.onXhrReady_);
        this.eventHandler_.listen(this.xhrManager_, goog.net.EventType.COMPLETE, this.onXhrComplete_);
    };

    ajaxsample.App.prototype.sendHoge = function() {
       this.sendRequest('get', {
                'text': 'hoge'
            }, goog.bind(this.hogeComplete, this), 'PUT', {
                '#text': name
            });
    };

    ajaxsample.App.prototype.hogeComplete = function() {
        console.log('hogeComplete!!!');
    };

    ajaxsample.App.prototype.sendRequest = function(path, query, callback, method, opt_content){
        var url = goog.Uri.parse('/closurelibrary/ajaxsample/' + path);
        goog.object.forEach(query || {}, function(value, key){
            url.setParameterValue(key, value);
        }, this);
        var headers = {}, body = null;
        if (opt_content) {
            body = goog.json.serialize(opt_content);
            headers['Content-Type'] = 'application/json';
        }
        return this.xhrManager_.send(this.nextXhrId_++, url, method, body, headers, 0, goog.bind(this.processRequest_, this, callback));
    };

    ajaxsample.App.prototype.processRequest_ = function(callback, e){
        var xhr = e.target;
        if (xhr.isSuccess()) {
            callback && callback(xhr.getResponseJson('while(1);'));
        } else {
            alert(xhr.getResponseText());
        }
    };

    ajaxsample.App.prototype.onXhrReady_ = function(e){
        if (this.xhrManager_.getOutstandingCount() == 1) {
            this.fadeOutIndicator_.stop(false);
            this.fadeInIndicator_.play(true);
        }
    };

    ajaxsample.App.prototype.onXhrComplete_ = function(e){
        if (this.xhrManager_.getOutstandingCount() == 1) {
            this.fadeInIndicator_.stop(false);
            this.fadeOutIndicator_.play(true);
        }
    };

    ajaxsample.App.getInstance();
});

最後にGoogleCodeのリポジトリのリンクを貼っておきます。
Google Code Archive - Long-term storage for Google Code Project Hosting.

上記以外の物も色々と置いてますが、参考程度に・・・。