#概要
Javscriptでクラスのメソッドをテストしようとしたときにthisの参照で失敗しました。
call
を使うことでthisを参照するクラスのメソッドもテストすることができたので、やりかたをまとめました。
#問題
以下の通り、test
関数と被テスト用のTested
クラスを定義します。
//テスト用関数
function test(func, arg, expected){
const result = func(arg);
if(result === expected){
console.log("OK");
}
else{
console.log("FAIL");
}
}
//被テストクラス
class Tested {
constructor(){
this.x = 3;
}
getX(){
return this.x;
}
}
test
はテストされる関数(func
)、テストされる関数に代入する引数(arg
)、期待する値(expected
)を引数として、func
にarg
を代入した結果とexpectedを比較した結果をコンソールに出力します。(なお、このテスト関数は2つ以上引数を取る関数に対応していなかったり、objectを出力する関数のチェックがうまくできなかったりして汎用性が低いのですが、本質ではないので、今回は気にしないことにします)
Tested
クラスはthis.x
の値を返すgetX
メソッドをもちます。this.x
にはコンストラクタで3が代入されているので、getXは原則3を返します。
let tested = new Tested();
console.log(tested.getX() === 3);
//true
一方で、tested.getX
の戻り値が3であるかをtest
関数で調べると、OKが出力されることが期待されますが、以下のエラーが返ってきます。
test(tested.getX, null, 3);
//Uncaught TypeError: Cannot read property 'x' of undefined
// at getX (<anonymous>:16:17)
// at test (<anonymous>:2:18)
// at <anonymous>:22:1
#原因
getX
のreturn this.x
の行で、「undefinedのxを読めない」と言われているので、this
がundefined
となっていることが原因だとわかります。
this
の仕様が難しくてすべてを理解できてはいないのですが、どうもtest
の引数としてtested.getX
を入れてしまったので、getX
が参照するthis
がtested
インスタンスではなく、test
関数になってしまい、x
の参照がうまく機能しないようです。
追記:undefined
になる理由がよく割らなかったのですが、関数が参照するthis
はStrictモードではundefined
になるという仕様があるらしく、どうもそれっぽいです。
(参考:JavaScriptのthisの挙動を完全に理解した話 )
#解決方法
Function.prototype.call()
を使うことで、クラスメソッドのテストを正しく行うことができました。
//tested.getXをfunction(_arg){return tested.getX.call(tested, _arg)}に置き換える
test(function(_arg){retrun tested.getX.call(tested, _arg)}, null, 3)
// OK
Function.prototype.call
は関数がthis
として扱うべき変数を第一引数に指定することができます。
今回はtested
をthis
として扱ってほしいので、tested.getX.call(tested, _arg)
とすればよいです。
ただしtested.getX.call
をそのままtest
関数の引数に持ってくることはできないので、_arg
だけを引数としてとる無名関数でラップします。
参考:
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/call
#おまけ
test
関数が複数引数を扱えるように拡張しておくことで、call
を無名関数でラップしなくてもよくなりコード量を削減できるかも、、、と思ったのですが、callを引数に指定するとどうもうまく機能しないようです。
//引数はfunc, arg1,arg2, ..., expectedのように指定する
function test(){
//複数引数に対応できるように、argumentsで引数を取得する
const _arguments = [].slice.call(arguments);
const func = _arguments[0];//最初の引数
const args = _arguments.slice(0,-1);//最初と最後以外の引数
const expected = _arguments[_arguments.length-1]//最後の引数
const result = func(...args); //...で引数のリストを展開する
if(result === expected){
console.log("OK");
}
else{
console.log("FAIL");
}
}
//さっきまでと一緒
class Tested {
constructor(){
this.x = 3;
}
getX(){
return this.x;
}
}
let tested = new Tested();
//callを使う分、引数がひとつ増えた扱いにして、テストする
test(tested.getX.call, tested, null, 3);
//VM352:9 Uncaught TypeError: func is not a function
// at test (<anonymous>:9:18)
// at <anonymous>:1:1
call
を別変数に代入して関数のように扱うことはできないようです。call
はprototype
のメソッド(?)として定義されますが、別変数に代入した時点でprototype
の参照先が変わってしまうためかもしれません(よくわかっていません)
#おまけ2
thisクラス側を書き換えることが可能な状況であれば、アロー関数を使ってメソッドを定義すれば、thisに悩まされなくて済みそうです。
今まであまり良くわかっていなかったのですが、functionとアローの違いはこういうところにあるんですね。
class Tested {
constructor(){
this.x = 3;
}
getX = () => {
return this.x;
}
}
let tested = new Tested();
console.log(test(tested.getX,3));
//true