UnitTest もどきを書いてみた

uupaa.js にはテスト用のフレームワークがありませんでした。
uupaa.js 0.7 は例により、フルスクラッチ(スクラップ?) & ビルド なので、いよいよテスティングフレームワークが必要に。

JsUnitなどの導入も検討したのですが、好みと違ったので uupaa.js の機能の一部として用意することに。

ステップ1 (HTMLファイルを用意する)

HTMLファイルを作ります。ファイル名の制限はありませんが、テストで使用する js ファイルと同じ名前 (例: uu.size.htm ) が分かりやすいと思われます。

<html><head><title>uu.size test</title>
<script src="../uupaa.debug.js"></script>
</head><body></body></html>

このとき、タイトル部分を「機能名 + " " + "test"」または 「機能名 + " " + "unittest"」にしておくと、

  • uu.feat("機能名") を裏で実行する。テストに必要な部品一式と、テスト本体の js ファイルがロードされる。
  • ロード完了後にテストが自動実行される

という うれしい特典が付いてきます。

自動実行ではなく任意のタイミングでテストを実行したい場合は、このようにします。

<html><head><title>適当なタイトル</title>
<script src="../uupaa.debug.js"></script>
<script>
function boot() {
  // なにか
  uu.test("uu.size");
}
</script>
</head><body></body></html>

ステップ2 (テスト本体の js ファイルを用意する)

js ファイルを作ります。ファイル名は、uu.feat で読み込まれることを考慮した名前にします。 (例: uu.size.js )
js ファイルの設置場所は、html ファイルと同じ場所がお勧めです。

ファイルフォーマットはこんな感じになります。テスト関数名は任意です。関数名は日本語でもOKです。

uu.feat[機能名] = {};

uu.mix(uu.test.problem, {
  テスト関数1: function() {
    return [左辺値, 比較演算子, 右辺値];
  },
  テスト関数2: function() {
    return [左辺値, 比較演算子, 右辺値];
  },
});

具体的にはこうします。

uu.feat["uu.size"] = {};

(function() {
var HASH1 = { aaa: 0, bbb: 1, ccc: 2 };

uu.mix(uu.test.problem, {
  size: function() {
    return [uu.size(HASH1), "===", 3];
  },
  first: function() {
    return [uu.first(HASH1), "===", 0];
  },
  "uu.toArray()の戻り値は配列でいいんだよね?": function() {
    return [uu.toArray(document.images), "is array"];
  },
});

})();

上の uu.size は Hash または 配列の要素数を求める関数で、uu.first は Hash または 配列の先頭の要素の値を返す関数です。
uu.toArray() は、Array や NodeList などを Array に変換します。

"===" は 厳密比較演算子で比較するという指示です。

左辺値と右辺値を取る演算子と、左辺値だけを取る演算子があります。
"ISNULL" などは右辺値が不要なので return [値, "ISNULL"]; とします。

比較演算子には、以下のものを用意しました。
"==", "===", "!=", "!==", ">", "<", ">=", "<=", "is true", "is false" "is NaN", "is 型名", "every", "some"

  • "===" と "!==" は リテラル以外の型(Hash や 配列)を与えると全要素の完全一致テストを行います。
  • 型名には、null, undef, undefined, hash, array, bool, boolean, str, string, num, number, func, function, fake, fakearray, rgba, rgbahash が指定できます。
  • "every" と "some" は Array.every と Array.some で各要素を評価します。
  • 演算子に含まれるスペースと大小文字の違いを無視します。"is null", "isNull", "ISNULL" を同じものとして扱います。

ステップ3 (実行)

実行するとこんな感じになります。

緑が「予定どおり」で、赤が「しくじった」です。

ワンポイント

uu.test() を使うと任意のタイミングでテストできます。
実行済みのテストは uu.test() で再テストされないため、インクリメンタルにテストを進めることもできます。

テストに成功したテスト関数は、uu.test.right[テスト関数名] にコレクションされます。失敗したテスト関数は uu.test.wrong[テスト関数名] にコレクションされます。
uu.test.clear() を実行するとコレクション( uu.test.right, uu.test.wrong)がクリアされ、再テストが可能になります。

おまけ

uu.assert() も用意しました。使い方は、 uu.assert(コメント, 左辺値, 比較演算子, 右辺値) または uu.assert(コメント, 値, 比較演算子) です。コメントには文字列を指定します。

console出力について

テストに失敗したり例外が発生した場合は、console.error() に出力します。

反省会

  • JsUnit は assert文の豊富(20種類ぐらい?)さと、名前が長すぎて好きになれなかった。
    • assertEvaluatesToFalse とか assertNotUndefined とか、まじめに打ち込んでたら指がつってまうよ。
      • 関数名を "test..." で始めなきゃいけない仕様や、テスト関数はグローバル関数でなければならない仕様は削った。おかげですっきりした。
  • 速度の測定は、専用の機能(uu.Class.Perf)があるのでそちらで。
  • setUp, tearDown, setUpPage は、活用できるシーンが少ないと思われるので、用意していない。
    • 足りない機能は後付する。

ソース

// === Unit Test ===========================================
// depend: array, node, styleSheet

uu.feat.test = {};

(function() {
var OPERATORS = {
      "==":       function(v1, v2)  { return v1 == v2; },
      "===":      function(v1, v2)  { return eq3(v1, v2); },// isNaN not support
      "!=":       function(v1, v2)  { return v1 != v2; },
      "!==":      function(v1, v2)  { return !eq3(v1, v2); },
      ">":        function(v1, v2)  { return v1 >  v2; },
      ">=":       function(v1, v2)  { return v1 >= v2; },
      "<":        function(v1, v2)  { return v1 <  v2; },
      "<=":       function(v1, v2)  { return v1 <= v2; },
      ISTRUE:     function(v1)      { return !!v1; },
      ISFALSE:    function(v1)      { return !v1; },
      ISNAN:      function(v1)      { return isNaN(v1); },
      ISNULL:     function(v1)      { return uu.type(v1) === UU.NULL;  },
      ISUNDEF:    function(v1)      { return uu.type(v1) === UU.UNDEF; },
      ISUNDEFINED:function(v1)      { return uu.type(v1) === UU.UNDEF; },
      ISHASH:     function(v1)      { return uu.type(v1) === UU.HASH;  },
      ISARRAY:    function(v1)      { return uu.type(v1) === UU.ARRAY; },
      ISBOOL:     function(v1)      { return uu.type(v1) === UU.BOOL;  },
      ISBOOLEAN:  function(v1)      { return uu.type(v1) === UU.BOOL;  },
      ISNUM:      function(v1)      { return uu.type(v1) === UU.NUM;   },
      ISNUMBER:   function(v1)      { return uu.type(v1) === UU.NUM;   },
      ISSTR:      function(v1)      { return uu.type(v1) === UU.STR;   },
      ISSTRING:   function(v1)      { return uu.type(v1) === UU.STR;   },
      ISFUNC:     function(v1)      { return uu.type(v1) === UU.FUNC;  },
      ISFUNCTION: function(v1)      { return uu.type(v1) === UU.FUNC;  },
      ISNODE:     function(v1)      { return uu.type(v1) === UU.NODE;  },
      ISFAKE:     function(v1)      { return uu.type(v1) === UU.FAKE;  },
      ISFAKEARRAY:function(v1)      { return uu.type(v1) === UU.FAKE;  },
      ISRGBA:     function(v1)      { return uu.type(v1) === UU.RGBA;  },
      ISRGBAHASH: function(v1)      { return uu.type(v1) === UU.RGBA;  },
      EVERY:      function(v1, v2)  { return v1.every(function(v) {
                                        return v2.indexOf(v) >= 0;
                                      }); },
      SOME:       function(v1, v2)  { return v1.some(function(v) {
                                        return v2.indexOf(v) >= 0;
                                      }); }
    };

// === operator
function eq3(v1, v2) {
  var i, n, type1 = uu.type(v1), type2 = uu.type(v2);

  if (type1 === type2) {
    switch (type1) {
    case UU.ARRAY:
    case UU.FAKE:
      return (uu.size(v1) === uu.size(v2) && v1.join(",") === v2.join(","));
    case UU.HASH:
      if (uu.size(v1) === uu.size(v2)) {
        for (i in v1) {
          if (v1.hasOwnProperty(i)) {
            if (i in v2) {
              if (v1[i] === v2[i]) {
                ++n;
              }
            }
          }
        }
        return uu.size(v1) === n;
      }
      return false;
    }
    return v1 === v2;
  }
  return false;
}

uu.mix(uu, {
  // uu.assert - assert
  assert: function(note, val1, operator, val2) {
    if (!UU.CONFIG.ASSERT) { return true; }

    var rv, ope = operator.toUpperCase().replace(/\s+/, "");

    if (!(ope in OPERATORS)) {
      if (console) {
        console.error(["unsupport operator:", operator, "in", note].join(" "));
        return false;
      } else {
        throw ["unsupport: ", note, operator].join(" ");
      }
    }

    rv = OPERATORS[ope](val1, val2);
    if (!rv) {
      if (console) {
        console.error(["assert:", note, val1, operator, val2].join(" "));
      }
    }
    return rv;
  },

  // uu.test - unit test
  test: function(src, path /* = "." */) {
    uu.feat(src, function() {

      var i, r;
      for (i in uu.test.problem) {
        try {
          if (i in uu.test.right || i in uu.test.wrong) {
            ; // already tested
          } else {
            r = uu.test.problem[i]();
            if (uu.assert(i, r[0], r[1], r[2])) {
              uu.test.right[i] = 1;
            } else {
              uu.test.wrong[i] = 1;
            }
          }
        } catch(err) {
          uu.test.wrong[i] = 1;
        }
      }

      uu.test.view();
    }, path || ".");
  }
});

uu.mix(uu.test, {
  // uu.test.problem - collection
  problem: {},

  // uu.test.right - tested collection
  right: { /* tested func, ... */ },

  // uu.test.wrong - tested collection
  wrong: { /* tested func, ... */ },

  clear: function() {
    uu.test.right = {};
    uu.test.wrong = {};
  },

  // uu.test.view
  view: function() {
    var i, j = 0, node = [];

    node.push('<div class="uutest_frame"><table class="uutest_table"><tr>');

    for (i in uu.test.problem) {
      if (j && !(j % 5)) {
        node.push('</tr><tr>');
      }
      if (i in uu.test.right) {
        node.push('<td class="uutest_right">', i, '</td>');
      } else {
        node.push('<td class="uutest_wrong">', i, ' !</td>');
      }
      ++j;
    }

    node.push('</tr></table></div>');

    uu.node.insert(node.join(""), uudoc.body);
  }
})

// --- auto run ---
// <title>{feat} test</title> or <title>{feat} unittest</title>
uu.ready(function() {
  var node, title, match;
  node = uu.tag("title");
  if (node.length) {
    title = node[0].textContent || node[0].innerText;
    match = title.match(/^\s*([\w\-+.,]+)\s*(?:test|unittest)$/);
    if (match) {
      uu.test(match[1]);
    }
  }
});

// apply table style
if (uu.style) {
  uu.style.insertRule(".uutest_frame",    "position:absolute;left:0;top:0");
  uu.style.insertRule(".uutest_table td", "padding:5px;border:1px dotted green");
  uu.style.insertRule(".uutest_right",    "color:white;background-color:green");
  uu.style.insertRule(".uutest_wrong",    "color:white;background-color:red");
}

})();

/* test example

--- [filename: uu.size.htm] --------------------------------
<html><head><title>test uu.size</title>
<script src="../uupaa.debug.js"></script></head><body></body></html>

--- [filename: uu.size.js] ---------------------------------
uu.feat["uu.size"] = {};

uu.mix(uu.test.problem, {
  size: function() {
    var a = { aaa: 1, bbb: 2, ccc: 3 };
    return [uu.size(a), "===", 3];
  }
});

 */