現在関わっているJavaScriptの仕事でのテストコードはMocha + Chaiを使って書いています。 ChaiのBDDスタイルでは
expect(1 + 1 == 2).to.be.true;
のような書き方になります。このコードって不思議ではありませんか?
expect(1 + 1).to.equal(2);
なら、expect
関数でテスト対象の1 + 1
を計算し、その結果が2
に等しいかをequal
関数で検証しています。しかし最初のコードではexpect
関数の値を検証する関数がありません。to
も be
、true
もexpect
関数の戻すオブジェクトのプロパティーの値です(プロパティーの値のオブジェクトのプロパティー、さらにそのプロパティーの・・・)。
なぜこんな表記なのか (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言語には関数呼び出しの()
を省略できるという珍しい特徴があります、上のコードに関数呼び出しの()
を追加すると以下のようになり、to
やbe
が関数(メソッド)だとわかります。
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の作成、詳細はリンク先を参照してください
- ③ : 元になるオブジェクト
true
、false
のプロパティーを持ちます - ④ : プロパティー参照時に実行される
getter(get)
の定義- 引数targetは、参照対象のオブジェクト
- 引数propは、参照されるプロパティー名
- ⑤ : getterの処理
- value(=
export
関数の引数)がプロパティーと等しければAssertion : OK
、等しくなければAssertion : NG
がコンソールに表示されます
- value(=
- ⑥ :
export
関数の定義- 引数valueは、テスト対象の値
- ⑦ :
export
関数の戻り値は、{to: {be: booleanProxyの値}}
- 今回は、
to
プロパティーの処理は省いています - 本物のChaiでは
to
プロパティーもProxyです
- 今回は、
- ⑧ : テストの記述1
- ⑨ : テストの記述2
まとめ
Proxyは通常のアプリケーションでは使う事はないと思いますが、フレームワークや基本的なライブラリーでは良く使われいる強力な機能です。
メタプログラミングが好きな方はぜひ使ってみてくさい、ただしご利用は計画的に😁