EY-Office ブログ

Chai アサーション フレームワークの不思議

現在関わっているJavaScriptの仕事でのテストコードはMocha + Chaiを使って書いています。 ChaiのBDDスタイルでは

expect(1 + 1 == 2).to.be.true;

のような書き方になります。このコードって不思議ではありませんか?

expect(1 + 1).to.equal(2);

なら、expect関数でテスト対象の1 + 1を計算し、その結果が2に等しいかをequal関数で検証しています。しかし最初のコードではexpect関数の値を検証する関数がありません。tobetrueexpect関数の戻すオブジェクトのプロパティーの値です(プロパティーの値のオブジェクトのプロパティー、さらにそのプロパティーの・・・)。

Chai

なぜこんな表記なのか (BDD)

そもそもなぜ expect(1 + 1 == 2).to.be.true このような記法で書くのでしょうか? assert.ok(1 + 1 == 2) でも良いのではと思う方もいると思います。
このように書くのはBDD(behavior driven development, ビヘイビア駆動開発)という、テストコードはプログラムの振る舞いを記述したコードだという思想から生まれてきた表記です。振る舞いを記述したものなので、より人間の言語(英語)に近い記法を目指しています。

BDDが知られるようになったのはRuby言語用の、RSpecからではないでしょうか? RSpecで今回のコードは以下のように書きます。
ちなみにこれをGoogle翻訳すると「(1 + 1 == 2)が真であることを期待する」になります。😁

expect(1 + 1 == 2).to be true

Ruby言語には関数呼び出しの()を省略できるという珍しい特徴があります、上のコードに関数呼び出しの()を追加すると以下のようになり、tobeが関数(メソッド)だとわかります。

expect(1 + 1 == 2).to(be(true))

ちなみに、以前のJavaScriptのプロジェクトでテストコードはJestを使っていました。Jestでは以下のようにRSpecに似せてますが、無理のない関数呼び出しのチェーンです。😅

expect(1 + 1 == 2).toBeTruthy();

どう実装されているか (Proxy)

さて expect(1 + 1 == 2).to.be.true;はどうやって検証処理を実行しているのでしょうか?

JavaScriptに詳しい方は Proxy を使っているのでは?と想像するかと思います、 実際にChaiのコードを見てみるとProxyを使っていました。
Proxyは、指定したオブジェクトの持つメソッドをフック(インターセプト)するコードを追加したオブジェクトを作成してくれます。Proxyを使うとプロパティー参照時に実行される処理を組み込めるのです。

実際のコードは複雑なのでexpect(1 + 1 == 2).to.be.trueを検証できる簡単なコード作ってみました。下のコードを実行するとコンソールにAssertion : OKが2回表示されます(⑧、⑨)。

const booleanProxy = (value) => {    // ← ①
  return new Proxy(                  // ← ②
    {true: true, false: false},      // ← ③
    {get: (target, prop) =>          // ← ④
      {console.log("Assertion : ", value === target[prop] ? "OK" : "NG");} // ← ⑤
    }
  );
};

const expect = (value) => {                // ← ⑥
  return {to: {be: booleanProxy(value)}};  // ← ⑦
};

expect(1 + 1 == 2).to.be.true;             // ← ⑧
expect(1 + 1 == 3).to.be.false;            // ← ⑨
  • ① : ③で定義されたオブジェクトをProxyしたオブジェクトを戻す関数の定義
    • 引数valueは、テスト対象の値
  • ② : Proxyの作成、詳細はリンク先を参照してください
  • ③ : 元になるオブジェクトtruefalseのプロパティーを持ちます
  • ④ : プロパティー参照時に実行されるgetter(get)の定義
    • 引数targetは、参照対象のオブジェクト
    • 引数propは、参照されるプロパティー名
  • ⑤ : getterの処理
    • value(= export関数の引数)がプロパティーと等しければAssertion : OK、等しくなければAssertion : NGがコンソールに表示されます
  • ⑥ : export関数の定義
    • 引数valueは、テスト対象の値
  • ⑦ : export関数の戻り値は、{to: {be: booleanProxyの値}}
    • 今回は、toプロパティーの処理は省いています
    • 本物のChaiではtoプロパティーもProxyです
  • ⑧ : テストの記述1
  • ⑨ : テストの記述2

まとめ

Proxyは通常のアプリケーションでは使う事はないと思いますが、フレームワークや基本的なライブラリーでは良く使われいる強力な機能です。
メタプログラミングが好きな方はぜひ使ってみてくさい、ただしご利用は計画的に😁

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ