音の鳴るブログ

鳴らないこともある

chai のプラグインを書いた & 書きかた

chai というアサーションライブラリがあって、テストフレームワークMocha と合わせてよく使われていると思うのだけど、その chai のプラグインを書いた。

このプラグインを使うと配列にも closeTo が使えるようになる。

expect([ 1.5, [ 2.5, 3.5 ], 4.5 ]).to.be.deep.closeTo([ 1, [ 2, 3 ], 4 ], 0.5);

chai のプラグイン

chai はプラグインで便利機能を拡張する前提で設計されていて、各種プラグインが公開されている。たとえば有名なのではテストスパイとかスタブを扱う Sinon.JS を chai のインターフェースで使える sinon-chai とかがある。他にもある → Chai Plugins

chai のプラグインの書きかた

簡単に書ける。プラグインを適用するには以下のように chai.use を使うのだけどそれに渡す関数を書くだけ。以下のコードは確認してないので、書き方だけ参考にしてください。

var chai = require("chai");

chai.use(function plugin(chai, _) {
  // ここにプラグインを書く
});

引数の chai というのは chai自体、_便利機能群 です。

新しいメソッドを追加する

chai のアサーションはクラスメソッドを使って拡張できるようになっていて、例えば新しいメソッドを追加したい場合は Assertion.addMethod を使う。

以下は絶対値が一致すれば OK というような absEqual を追加する場合。 _.flag というのは内部状態を参照したり変更したりする補助関数で、ここでは msg があればメッセージをセットするのと、アサーション対象の値をゲットするのに使っている。

function plugin(chai, _) {
  var Assertion = chai.Assertion;

  Assertion.addMethod("absEqual", function(val, msg) {
    if (msg) {
      _.flag(this, "message", msg);   // メッセージをセット
    }
    var obj = _.flag(this, "object"); // アサーション対象の値をゲット
    this.assert(
      Math.abs(val) === Math.abs(obj),
      "expected #{this} to equal #{exp}",
      "expected #{this} to not equal #{exp}",
      val // この値は ↑ の #{exp} に使われる
      // ここに値を書くと ↑ の #{act} に使われる
    );
  });
}

完璧とは言いがたいけど、これでこういうアサーションが追加される。

expect(-10).to.absEqual(10); // OK

新しいプロパティを追加する

プロパティの追加は Assertion.addProperty を使う。以下は NaN のチェックをするやつ。

function plugin(chai, _) {
  var Assertion = chai.Assertion;

  Assertion.addProperty("NaN", function() {
    this.assert(
      isNaN(_.flag(this, "object")),
      "expected #{this} to be NaN",
      "expected #{this} to be not NaN"
    );
  });
}
expect(-10).to.be.NaN; // "expected -10 to be NaN"

ちなみにアサーションは必ず必要というわけでなくて、フラグをセットするだけとか新しいアサーションを作るとかでも良い。 以下は配列をソートしてから以下に続けるやつ。

function plugin(chai, _) {
  var Assertion = chai.Assertion;

  Assertion.addProperty("sort", function() {
    var obj = _.flag(this, "object");

    obj = obj.slice().sort();

    return new Assertion(obj, _.flag(this, "message"));
  });
}
expect([ 5, 1, 3, 2, 4 ]).to.sort.and.eql([ 1, 2, 3, 4, 5 ]); // OK

既存のメソッドを上書きする

上書きはちょっと複雑で元のメソッド付きで関数が呼ばれるので、その中で上書き後の関数を返す。たとえば以下は closeTo を上書きして actual と expected が一致する場合は equal で評価して、そうでない場合はオリジナルの closeTo を使うやつ。

function plugin(chai, _) {
  var Assertion = chai.Assertion;

  Assertion.overwriteMethod("closeTo", function(_super) {
    return function(expected, delta, msg) {
      var obj = utils.flag(this, "object");

      if (obj == expected) {
        return this.equal(expected, msg);
      }

      return _super.apply(this, arguments);
    };
  });
}
expect(Infinity).to.be.closeTo(Infinity, 0.5); // OK

だいたいこういう感じ。

あとは node.js とブラウザ両方で使えるようにするとか mocha コマンドのときにプラグインを適用するとかあるのだけど、面倒なので興味があれば僕の書いたプラグインを参考にしてください。

気づき

バニラアイスにコーヒー豆を 2, 3粒挽いたのをまぶすと美味しい

参考

デフォルトのインターフェースも同じようにクラスメソッドを介して拡張されている

Plugin Utility API のドキュメント

上に書かれていないAPIもある