0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Javascript】thisを参照するクラスメソッドのテスト

Last updated at Posted at 2021-07-18

#概要
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)を引数として、funcargを代入した結果と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

#原因
getXreturn this.xの行で、「undefinedのxを読めない」と言われているので、thisundefinedとなっていることが原因だとわかります。

thisの仕様が難しくてすべてを理解できてはいないのですが、どうもtestの引数としてtested.getXを入れてしまったので、getXが参照するthistestedインスタンスではなく、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として扱うべき変数を第一引数に指定することができます。
今回はtestedthisとして扱ってほしいので、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を別変数に代入して関数のように扱うことはできないようです。callprototypeのメソッド(?)として定義されますが、別変数に代入した時点で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
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?