【Javascript】テスト用の関数の自作

#概要
Javascriptで自作関数の出力をチェックをしたくなったときに、ある程度汎用的に使えるテスト用関数を作りました。
意識したことは以下です。

* 被テスト関数の引数の個数を限定しない
* リストやオブジェクトを返す関数の場合も、要素同士の異同を判定できるようにした


#実装

```Javascript:js/Tester.js
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`はパブリックな関数を２つもつクロージャです。
`exec`は一回のテストの結果を真偽値で返します。
`testAll`は被テスト関数に代入したい引数と期待出力のセットのリストを渡すことで、複数のテスト結果をコンソール出力することができます。

クラスで実装しても良かったのですが、`this`のややこしさを避けるためと、パブリックにする関数を絞るためにクロージャで実装しました。
##_this

```Javascript
   const _this = {}
```
クロージャ内関数が自由にアクセスできる`this`的な変数として`_this`を用意しました。
クロージャなので別になくてもよいのですが、`_this`に紐付け可能なものは紐付けておいたほうが、クロージャ内で宣言された変数/関数かどうかが判断しやすいので用意しました。

##可変長引数
`exec`は関数`_func`に引数`_args`を代入したときの出力を期待出力`expected`と比較し、真偽値を返します。
関数内で`arguments`を用いることで、_argsが２つ以上のときも対応可能できるようにしました。(exec(func, arg1,arg2,...,expected)のような形で実行できます）

また`func`には、`...`を使って`args`を展開することで、`func`が取る引数の個数によらずうまく渡せるようにしました。

```Javascript
        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`など一部の特殊な型はうまく比較できないので注意が必要です。

```Javascript
	_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`として判定されても問題ないのですが）、より細かく取得できる関数を定義しています。

```Javascript
	_this.typeOf = function(obj){
		return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
	}
```

参考：
[JavaScriptの型などの判定いろいろ](https://qiita.com/amamamaou/items/ef0b797156b324bb4ef3)

#テスト
ブラウザのコンソール出力でテストします。

```html:index.html
<script type="module" src="js/main.js"></script>
```

```Javascript:js/main.js
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
]);
```

被テスト関数の引数がいくつでも正しく比較できています。
また、被テスト関数の戻り値がリストやオブジェクトの場合でも正しく比較できています。