概要
Javascriptで自作関数の出力をチェックをしたくなったときに、ある程度汎用的に使えるテスト用関数を作りました。
意識したことは以下です。
- 被テスト関数の引数の個数を限定しない
- リストやオブジェクトを返す関数の場合も、要素同士の異同を判定できるようにした
実装
function Tester(){
const _this = {}
_this.exec = function(_func, _args, _expected){
const _arguments = [].slice.call(arguments);
const func = _arguments[0];
const args = _arguments.slice(1, -1);
const expected = _arguments[_arguments.length-1];
const value = func(...args);
//console.log(value, args,expected);
if(_this.isSame(value, expected)){
return true;
}
else{
return false;
}
}
_this.testAll = function(func, data){
let ok = true;
for(let d of data){
const args = d.slice(0,-1);
const value = func(...args);
const expected = d[d.length-1];
if(_this.isSame(value, expected)){
console.log("OK: args=",JSON.stringify(args), JSON.stringify(value), "=",JSON.stringify(expected));
}
else{
console.log("NO: args=",JSON.stringify(args), JSON.stringify(value), "!=",JSON.stringify(expected));
ok = false;
}
}
return ok;
}
_this.typeOf = function(obj){
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
_this.isSame = function(object1, object2){
const type1 = _this.typeOf(object1);
const type2 = _this.typeOf(object2);
if(type1 !== type2)return false;
if(["undefined","null","boolean","number","string"].includes(type1)){
return object1 == object2;
}
else if(type1 === "array"){
if(object1.length != object2.length)return false;
for(let i=0;i<object1.length;i++){
if(_this.isSame(object1[i], object2[i]) === false)return false;
}
return true;
}
else if(type1 === "object"){
if(object1.length != object2.length)return false;
for(let k in object1){
if(k in object2 == false) return false;
if(_this.isSame(object1[k], object2[k]) === false)return false;
}
return true;
}
else{//mapとかsetとかsymbolとか
console.log("warning: unsupposed type", type1);
return object1 === object2;
}
}
return {
exec: _this.exec,
testAll: _this.testAll
}
}
export { Tester }
解説
Tester
Testerはパブリックな関数を2つもつクロージャです。
execは一回のテストの結果を真偽値で返します。
testAllは被テスト関数に代入したい引数と期待出力のセットのリストを渡すことで、複数のテスト結果をコンソール出力することができます。
クラスで実装しても良かったのですが、thisのややこしさを避けるためと、パブリックにする関数を絞るためにクロージャで実装しました。
_this
const _this = {}
クロージャ内関数が自由にアクセスできるthis的な変数として_thisを用意しました。
クロージャなので別になくてもよいのですが、_thisに紐付け可能なものは紐付けておいたほうが、クロージャ内で宣言された変数/関数かどうかが判断しやすいので用意しました。
可変長引数
execは関数_funcに引数_argsを代入したときの出力を期待出力expectedと比較し、真偽値を返します。
関数内でargumentsを用いることで、_argsが2つ以上のときも対応可能できるようにしました。(exec(func, arg1,arg2,...,expected)のような形で実行できます)
またfuncには、...を使ってargsを展開することで、funcが取る引数の個数によらずうまく渡せるようにしました。
const _arguments = [].slice.call(arguments); #argumentsはlist-like変数なので一応listに変換する
const func = _arguments[0];
const args = _arguments.slice(1, -1);
const expected = _arguments[_arguments.length-1];
const value = func(...args);
リストやオブジェクトの比較
arrayやobjectの比較はアドレスの比較となるため、そのままだと中身が一緒でもfalseになってしまいます。
そこでisSameという比較用の関数を作りました。
isSameではtypeOf関数で引数のタイプを特定したあと、タイプがarrayやobjectであれば、要素ごとの比較を再帰的に行うことで、要素同士が一致しているかどうかの比較をできるようにしました。
ただし完璧ではなく、mapなど一部の特殊な型はうまく比較できないので注意が必要です。
_this.isSame = function(object1, object2){
const type1 = _this.typeOf(object1);
const type2 = _this.typeOf(object2);
if(type1 !== type2)return false;
if(["undefined","null","boolean","number","string"].includes(type1)){
return object1 == object2;
}
else if(type1 === "array"){
if(object1.length != object2.length)return false;
for(let i=0;i<object1.length;i++){
if(_this.isSame(object1[i], object2[i]) === false)return false;
}
return true;
}
else if(type1 === "object"){
if(object1.length != object2.length)return false;
for(let k in object1){
if(k in object2 == false) return false;
if(_this.isSame(object1[k], object2[k]) === false)return false;
}
return true;
}
else{//mapとかsetとかsymbolとか
console.log("warning: unsupposed type", type1);
return object1 === object2;
}
}
変数の型の取得
isSameのなかで変数の型を取得しています。通常のtypeofだとarrayがobjectとして検出されるなど判定が少し粗いので(実は今回の場合はarrayはobjectとして判定されても問題ないのですが)、より細かく取得できる関数を定義しています。
_this.typeOf = function(obj){
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
テスト
ブラウザのコンソール出力でテストします。
<script type="module" src="js/main.js"></script>
import { Tester } from "./Tester.js";
//可変長引数の関数
function sum(){
const args = [].slice.call(arguments);
let cnt = 0;
for(let v of args)cnt += v;
return cnt;
}
//リストやオブジェクトを返す
function getList(x){
return [x,2,3];
}
let tester = new Tester();
tester.testAll(sum, [
[1,2,3],//OK
[1,2,4],//FAIL
[1,1],//OK
[0]//OK(引数ゼロの場合
]);
tester.testAll(getList, [
[1, [1,2,3]],//OK
[[1,2],[[1,2],2,3]],//OK
[{a:3},[{a:3},2,3]],//OK
[{a:3},[{b:3},2,3]]//FAIL
]);
被テスト関数の引数がいくつでも正しく比較できています。
また、被テスト関数の戻り値がリストやオブジェクトの場合でも正しく比較できています。