📝

Function.prototype.bind()の戻り値をsinon.jsでテストする方法

2021/07/04に公開

こんにちは。最近はmocha変えてテストを頑張っています。今回はFunction.prototype.bind()の戻り値をテストする方法についてです。

どういう状況?

function Directive() {
  // ..
}

Directive.prototype.compile = function(elem, attrs, transclude) {
  return this.link.bind(this);
};

Directive.prototype.link = function($scope, elem, attrs) {
  // ...
};

AngularJSでの一幕ですが、APIの仕様上compile()link()を返すことで2つの処理が行われるようになっています。私はTypeScriptで書いているので、あまりオブジェクトと関数のネストを好まずクラスとメソッドで書くため、このようなprototypeの多用が起こります。

何をテストしたい?

Directive#compile()Directive#link()を「実行するのではなく、第一級関数として返す」ことを検証したいのです。

考えたアプローチ

以前書いた拙記事に「Function型の変数同士の比較」というものがありました。今回も同じようにString()で文字列にして検証してやろうとしたわけです。(少し行儀悪い気もしますが)

これには問題がありました。.bind()をすると結果の関数オブジェクトに対してString()を行っても"[native code]"しか返さないのです。もちろん検証は不可能。

Sinon.JSのstubを使ってみた

一か八か、Sinon.JSでなんとかしようと試みました。結果としては成功です。

var Directive: any;
beforeEach(() => {
  Directive = new DirectiveStatic();
});

// ...

  it('compile() は link() 関数を返す', () => {
    var DirectiveStub = {
      link: sinon.stub(Directive, 'link')
    };
    var result = Directive.compile(elem, attrs, transclude);
    result();
    assert(DirectiveStub.link.callCount === 1);
  });

beforeEach()Directiveのインスタンスを生成しますが、it()の段階でスパイを作っても全然問題ないようです。DirectiveStaticというのは、実装側からインポートする際の変数名です。気にしないで。

sinonにはsinon.spysinon.stubという2つの代表的な使用法がありますが、ここでspyにしてしまうと、このテストでは求めていないDirective#link()の処理まで走ってしまいます。そうさせないためにstubとします。

あとは受け取ったresult()を付けて実行させれば完了。callCountを検証します。ここでresult(); result();とするとcallCountは2になり、何もしないと0のままなので、検証の意味は果たしていると判断しました。

どうやら戻り値がthis(つまりDirectiveインスタンス)でバインドしたあとの関数だったおかげでスタブが活きたようです。面白いなー。


ニッチなテクですが、もしこの問題に直面したら思い出してみてください。

Discussion