Jan 22, 2007

MozUnitで単体テスト

MozLabに含まれてる単体テストツールのMozUnitの使いかたが分からない、っていうか真面目にプログラミング勉強したことないから単体テストっていうのがどういう物なのかすらわかってない。説明が全然ないし、普通に他のテスト用のツールを使い慣れた人でないと使えないとかそういう物なのだろうか?

とりあえず試しに動かしてみたけど、何が嬉しいのかいまいち分からない。隠し設定使えば普通にJSコンソールにエラーがリアルタイムで表示されるし、エラー表示だけじゃどこがいけないのか分からないとかいっても結局の所JSデバッガで一行ずつ見ていかないと分からない所だってあるし……自動制御で動かすのも、userChrome.jsとかで自分でスクリプト書いてやるのとの違いが分からない。特に、ブラウザの読み込みが完了したタイミングでのテストとか。

日曜プログラマもどきみたいなのが手を出していい物じゃあないんじゃないのか、これは。

追記。MozUnitのソースコード見てああでもないこうでもないと頭の中で検証してやっと使い方が理解できた。

動画で紹介されてるのは非同期処理の例なんだけど、これで例えばブラウザの読み込みを待ってからテストを実行するとかそういうのをやろうとすると、全然うまくいかない。

こういう場合はコールバック関数とかイベントリスナとかを使って、テストしたい状態になるのを待ってからテストしてやらなきゃいかんのだけど、その方法が全然分からなかった。MozUnitのページには、何やらオプションを指定すると非同期でない(変な日本語だ……)テストができるとか書いてあるんだけど、英語だし略語と技術的専門用語との区別がつかんしで、ちんぷんかんぷんだった。

結論から言うと、async型のテストを作る場合は「setUp」メソッドに渡されるコールバック関数を使うというのが鍵だった。このコールバック関数を実行しないと、処理が一歩も進まない。

以下、実際に試してみたテストケースの例。


var TestCase = mozlab.mozunit.TestCase;
var assert = mozlab.mozunit.assertions;

/* XPConnect特権付きで実行されるので、
   XPCOMコンポーネントも普通に利用できる。 */
var WindowManager = Components
               .classes['@mozilla.org/appshell/window-mediator;1']
               .getService(Components.interfaces.nsIWindowMediator);
var win = WindowManager.getMostRecentWindow('navigator:browser');

// var t;
// var t = null;
var t = {};
/* クロージャ用に変数の宣言だけしておこうとすると、実行時に
   エラーになる。
   (実行時にTestCaseのインスタンスを探すために、このファイルの
     すべてのグローバル変数のプロトタイプチェーンを辿ろうとしてしまうため、
     undefinedな変数があるとエラーになる)
   なので、グローバル変数は面倒でもすべて何らかのオブジェクトにしておく
   必要がある。 */

var count = 0;
var uris = [
    'http://www.google.com/',
    'http://spreadfirefox.jp/foxkeh/',
    'http://piro.sakura.ne.jp/xul/unknown/'
  ];

/* {runStrategy: 'async'} というオプションをコンストラクタの
   第2引数に指定するとコールバック関数を使ったテストが可能になる。 */
var testcase = new TestCase('this is a test!', {runStrategy: 'async'});

testcase.tests = {
  /*  "setUp" はテストが実行される前に毎回呼ばれる。
      ここに渡されたコールバック関数に対して"ok"という文字列を渡すと、
      テストが実行される。
      "ko"という文字列を渡すと、テストを行わずに処理を進める。
      (テストを行わず "tearDown" を実行する。
        テストのカウンタは進まない。) */
  setUp : function(aCallback) {
    t = win.gBrowser.addTab(uris[count++]);
    t.linkedBrowser.addEventListener('load', function(aEvent) {
      t.linkedBrowser.removeEventListener('load', arguments.callee, true);
      aCallback('ok');
    }, true);
  },

  /* "tearDown" はテストが完了する度に毎回呼ばれる。
     テストが成功しても失敗しても、ここに処理が回ってくる。
     テストで行った処理の後始末はここでやる。 */
  tearDown : function() {
    win.gBrowser.removeTab(t);
  },

  // 以下、テストの定義。

  'test1: normal page load' : function() {
    // ページがちゃんと読み込まれていればテストをパスする
    assert.equals(t.linkedBrowser.contentTitle, 'Google');
  },

  'test2: redirected page load' : function() {
    // きちんとリダイレクトされていればテストをパスする
    assert.equals(t.linkedBrowser.currentURI.spec, 'http://foxkeh.jp/');
  },

  'test3: ' : function() {
    // こんなリダイレクトは無いのでテストをパスしない
    assert.equals(t.linkedBrowser.currentURI.spec,
                         'http://piro.sakura.ne.jp/xul/_unknown.html');
  }
}

たったこれだけのことを理解するのに半日費してしまったよ……無能な自分が嫌になる。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能