QUnitを導入してみた。

 QUnit - jQuery JavaScript Library

 ページの遷移周りをすっかりJavaScriptに頼るようになってしまったし、それに大活用させていただいてるjQueryさんの開発も未だ衰える事なくアクティブですし、相変わらず凡ミスやらかしそうだし、テストって大事ですよね、テストテストテスト。後から楽できるしね、テストテストテスト。とか思ったかどうかは判然としませんが、一応当ページのテストにもQUnitを導入してみました(まぁ、例によって随分と前の話でありますけど)。ちなみに、サーバサイドの方はDjangoを使っているので純正のテストツールがあります。最初の頃書いてなかったので抜けが多いけど。あ、最近も書いているとは言いがたいけど。

 もともとはjQuery用でjQueryに依存した実装であったらしいのだけど、今ではすっかりスタンドアロンなライブラリになっているらしいです。と言っても、ボクの方がjQueryさんに依存しているので元の木阿弥なんだけどね。Webブラウザ以外のJavaScriptエンジン的なものでも動作するようになっているとの事だけども、これまた私はブラウザで動かして結果も確認するやり方を選択してみました。公式ドキュメントがそうだったから。

 そうそうQUnitさんもgithubにて公開されていますよ。有り難いですね。

 jquery/qunit - GitHub

 QUnitを導入するには上記リポジトリからダウンロードして配置してしまえばいいのですけど、CDNとして配信もされているのでそちらから直接読み込んでみました。こうするとお手軽ではありますが、バージョンなどの指定はできませんのである日突然いきなりあちらの都合で動かなくなる可能性もあります。テストだからまぁ、いいかと思いましたけど。あと、一緒にサンプルとしても配布されている各テスト書類はQUnit自体をテストするものですね :-)

 QUnitさん本体と結果を美しく表示してくれるCSSを読み込んで、それらを適用させるべくタグを仕込んだテンプレートは以下のようになりました。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>QUnit Sample.</title>
  <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen" />
</head>
<body>
  <h1 id="qunit-header">QUnit Sample</h1>
  <h2 id="qunit-banner"></h2>
  <div id="qunit-testrunner-toolbar"></div>
  <h2 id="qunit-userAgent"></h2>
  <ol id="qunit-tests"></ol>
  <div id="qunit-fixture"></div>
  <script src="http://code.jquery.com/qunit/git/qunit.js"></script>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
</body>
</html>

 要素のIDにqunit-とプリフィックスが付いているものがQUnit用の要素です。これにより折りたたみ式で奇麗なテスト結果閲覧ページが表示されるようになります。IDがqunit-fixtureという要素内はテストを実行する度に初期化してくれる領域で、例えばあるイベントで表示内容を差し替えるみたいな場合に使うと便利だと思います。自前で元に戻す処理が必要になりませんから。ただし、元に戻される事で生じる不都合もありますから何でもここに入れてしまえばいいというわけではありません。

 後は、このファイルにテストに必要な要素を書き加えたり、外部ファイルを読み込んだりします。今回は別のファイルに書いて読み込む方式を採用しましたが、テスト自体を書き込んでもOKです。

 実行されるテストはunit test風で、他のunit testを使った事があればすんなり馴染めるものだと思います。それをjQuery風に記述していきますが、基本的にできる事は真偽と内容の一致判定と例外の捕捉の三つです。それらの確認処理をそのまま実行する場合にはtest()へ、Ajax等を用いていて時間差で実行したい場合にはasyncTest()へ渡します。その時、expect()も一緒に渡せば中で実行されるテストの数をチェックする事もできます。増やす度に書き換えなければならないので手間ですが、何らかの利用でテストが実行しなくなった場合でもすぐ気が付けるようになります。module()に続けてtest()やasyncTest()を書く事でグループ化する事もできます。その場合、次にmodule()が登場するまで同じグループとみなされるので注意が必要ですが、同じグループ内のテストに前処理(setup)と後処理(teardown)を実行させるようにもできます。

var bool = true;
var num = 1;
var str = 'a';
var obj = {};
var obj2 = obj;

 と定義されているとしていつくか例を書いてみますと、

test('basic', function () {
    expect(8);
    ok(bool, 'bool');
    deepEqual(num, 1, 'num');
    notDeepEqual(num, '1', 'num is not String');
    deepEqual(str, 'a', 'str');
    deepEqual(obj, {}, 'obj');
    strictEqual(obj, obj2, 'obj referensed obj2');
    notStrictEqual(obj, {}, 'obj is not {}');
    raises(function () {
        throw new Error();
    }, 'throw error');
});

 みたいになりました。基本的な形は関数名(実際値、期待値、メッセージ)とかですね、違うのもありますけど。ok()が真偽値を判定するメソッドで、equalが含まれるのが同一かどうか評価するメソッド、そしてraises()が例外を扱います。deepEqula()とstrictEqual()の違いは名前からもわかるように、JavaScriptの“==”と“===”の違いです。deepEqual()だと型変換が行われて評価されますが、strictEqual()だとされません。

 次にテンプレートへ、

<div id="testBox"></div>

<script src="http://code.jquery.com/jquery-latest.js"></script>

 を追加し、

function loadTestBox() {
    $.getJSON('test.json', function (json) {
        $('#testBox').text(json['string']);
    });
}

 というAjaxを用いた関数をテストしてみます。test.jsonの中身は、

{
    "string": "json data",
}

 こうなっているので、件の関数を呼び出されたらIDがtestBoxの要素が内包するテキストがjson dataとなるはず、ただしAjaxを用いるので即時反影されるとは限らず、若干の遅延が生じる可能性がある。とかまぁ、前置きが長いですが、そういう時のメソッドがちゃんと用意されているので、型通り書いていけば大丈夫です。

asyncTest('getJSON', function () {
    expect(1);
    loadTestBox();
    setTimeout(function () {
        deepEqual($('#testBox').text(), 'json data');
        start();
    }, 100);
});

 ざっくり言えば、setTimeout()を使って評価したい処理の実行後任意のタイミングを置いてからテストを実行する感じですか。

 次にEvent周りのテストをやってみます。と言っても特に変わった事をする必要はあまりなくて、

$(function () {
    $('#clickTestBox').click(function () {
        $.getJSON('test.json', function (json) {
            $('#testBox').text(json['click']);
        });
    });
    $('#hoverTestBox').hover(
        function () {
            $(this).text('hover');
        },
        function () {
            $(this).text('out');
        });
});

 と、IDがclickTestBoxの要素をクリックするとIDがtestBoxの要素の中身が変わり、マウスオーバーで内容が変わるIDがhoverTestBoxの要素をテストする場合は、

asyncTest('click', function () {
    expect(1);
    $('#clickTestBox').click();
    setTimeout(function () {
        deepEqual($('#testBox').text(), 'click');
        start();
    }, 100);
});
test('hover', function () {
    expect(2);
    var overFunc = function () {
       
    };
    $('#hoverTestBox').mouseover();
    deepEqual($('#hoverTestBox').text(), 'hover');
    $('#hoverTestBox').mouseout();
    deepEqual($('#hoverTestBox').text(), 'out');
});

 というふうに書けました。Event周りはもっといろいろ込み入った場合もあるので、こう単純にはいけない事も多々あると思いますが、実際にjQuery UIなどもQUnitでテストをされているらしいので、もっともっと込み入った事へも対応できるのだと思います。私はそこまで頭が至りませんけども。

 最後にmodule()を使ったsetup、teardownの例。

module('module1', {
    setup: function () {
        console.log('module1 setup...');
    },
    teardown: function () {
        console.log('module1 teardown...');
    }
});
test('module1 dummmy', function () {
    expect(0);
    console.log('module1 dummy');
});
test('module1 dummy2', function () {
    expect(0);
    console.log('module1 dummy2');
});

 と書いて実行したときのコンソールログには、

module1 setup…
module1 dummy
module1 teardown…
module1 setup…
module1 dummy2
module1 teardown...

 と表示されておりました。ふふふ、思った通りだ。

 あ、今回のサンプルはgithugに置いているので、不明な点などがありましたら参照してください。

 tactactad/QUnitSample - GitHub

 Pythonとかのdoctestも好きですがunit testも嫌いではないですし、それなりに他のunit test環境の流儀を持ちこんでいたりもするので凄く好感を抱きましたよ。私は頭も悪いですけども手離れも悪くて、とりあえず動くようになった後からちょっとずつ手を加えてすっきりさせるみたいな事がよくあるので、最低限の動作を確認、保証できるこの手のツールは大変ありがたいです。これからもガンガン活用していこうと思います!