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" を同じものとして扱います。
ワンポイント
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..." で始めなきゃいけない仕様や、テスト関数はグローバル関数でなければならない仕様は削った。おかげですっきりした。
- assertEvaluatesToFalse とか assertNotUndefined とか、まじめに打ち込んでたら指がつってまうよ。
- 速度の測定は、専用の機能(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]; } }); */