コンテンツ
はじめに
Mochaのフレームワークに従うテストコードの構造とコールバックに関する備忘録。基本ではあるが、理解していないと思わぬところで足をすくわれるかも知れない。(筆者も含めて) なんとなく使っているという人は、一度基本を確認して見るのも悪くはないかも。
Mochaとは何か
そもそもの話として …
MochaはJavaScriptを対象としたテストフレームワーク。
Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun.
https://mochajs.org
テスト対象は、ローカルにある単体の関数からリモートのRest APIまで、テストコードから呼び出し可能な機能であれば何でも良い。
テストコードは、テスターがJavaScriptのコードとして記述する。Mochaはテストコードの実行や、テスト結果の報告を管理してくれる。
Mochaの特徴は、テストコードにテスト仕様を記述できること。これによりTDD (Test Driven Development) やBDD (Behavior Driven Development) といった開発プロセスとの親和性が高いテストができる。それぞれのスタイルに対応したインタフェース(語彙)が用意されている。
Mochaの実行
MochaはCLI (Command Line Interface) として実行できる。コマンドラインからMochaを実行すると、テストコードの記述されたファイルを読み込み実行する。
Mochaに読み込むファイルの指定には様々な方法がある。デフォルトは./test
ディレクトリにあるファイルとなる。対象のディレクトリに複数のファイルがあれば、まとめて実行してくれる。逆に個別に指定することもできる。
コマンドラインからの実行の他、ブラウザの中でも利用できる。
Nodeの開発環境でのMochaの実行
Nodeの開発環境では、ローカルにインストールしたMochaの実行ファイルは下記の場所にある。
./node_modules/mocha/bin/mocha
これはnpm
からだと簡単に起動できる。npm
でインストールされたモジュールにbin
ディレクトリがあると、./node_modules/.bin
の下に元のbin
ディレクトリのコマンドを起動するスクリプトが作成される。npmの実行コンテキストでは、このディレクトリにパスが通っているため、npmからは対象のスクリプトを直接起動できる。Mochaを実行したいディレクトリへの変更も含めて、package.json
に下記のように記述すれば良い。
scripts: {
test: "cd <dir> & mocha"
}
これを起動するにはコマンドラインから下記のように実行する。
npm test
VS Codeでのデバッグ
MochaのテストコードをVS Codeでデバックする方法が解説されている。
Mochaによるテストコードの基本構造
ここからが本題。
Mochaに読み込まれるテストコードは下記のようなdescribe
とit
が組み合わさった構造を持つ。
describe('テストの大見出し', function() {
describe('テストの小見出し', function() {
it('テスト内容の説明', function() {
// テストコード
// 機能の実行
// 機能の検証
// 例外の送出 (もし検証結果が偽であれば)
});
it('テスト内容の説明', function() {
// テストコード
// 機能の実行
// 機能の検証
// 例外の送出 (もし検証結果が偽であれば)
});
});
describe('テストの小見出し', function() {
});
});
因みにdescribe
やit
はBDD向けのインタフェース。TDDだとそれぞれsuite
とtest
になる。
it
it
はテストの最小単位である。it
の中でテスト対象の機能を実行し、その結果を検証する。検証結果のエラーは、例外として通知される必要がある。it
はその例外をキャッチして、テスト結果として保存し、テスト実行後にレポートする。次に記述するdescribe
と違い、ネストすることはできない。
describe
describe
は、複数のit
をまとめられる。また階層化(ネスト)してテストを構造化できる。
describe
には、before
/after
/beforeEach
/afterEach
といったHookを設定できる。Hookはユーザーの用意する関数で、テストの前処理、後処理を行える。
before
/after
はdescribe
の実行の前処理、後処理であり、beforeEach
/afterEach
は、describe
の中の各it
の実行に対する前処理、後処理となる。
it
ごとに前処理、後処理の内容を変えたい時はどうするのか? beforeEach
/afterEach
の中で工夫できるかも知れないが、素直に1つのdescribe
に1つのit
を割り当てるほうが良さそう。
コールバック
it
の中のテストコードは、コールバックとして記述されることに注意する。it
のブロック間にコードを記述することは可能だが、実行順は見かけ上の記述の順番にはならない。これはdescribe
についても同様である。ブロックという呼び方も誤解を招くかも知れない。it
やdescribe
は関数だし、それらのコールバックの関数なので、ブログラム内の{}によるブロックとは異なる。
describe
とit
で、コールバックの実行順序は下記のようになる。
describe
のコールバックは、そのdescribe
の中で実行されるdescribe
がネストしている場合は、describe
のコールバックの実行もネストする (親->子->親)
it
のコールバックは、全てのdescribe
が処理されたあとに実行される- 同じ
describe
の中にあるit
のコールバックは、定義されている順番に実行される describe
がネストしている場合は、親のレベルのit
のコールバックが全て実行されたあとに、子のレベルのit
のコールバックが実行されるdescribe
のネストとは実行順が異なる
この確認に使ったテストプログラムを下記に示す。
途中変数value
に値を入れ直しながら、コールバックの中で参照している。value
の値は、当然ながら上記のルールに従って変化する。
describe
とit
でコールバックの実行タイミングが異なるところが要注意。各it
の中で閉じた処理を書くように心がけていれば問題はないはずだが、it
から外側のdescribe
で定義された変数を参照するようなケースでは要注意である。
let value = '!!';
console.log(`世界の始まり - ${value}`);
describe('実行順の確認A-C', function () {
console.log(`A-Cの始まり - ${value}`);
value = 'A';
it('テストA', function () {
console.log(`テストAの中 - ${value}`);
value = 'AA';
});
console.log(`A-Bの中間 - ${value}`);
value = 'B';
it('テストB', function () {
console.log(`テストBの中 - ${value}`);
value = 'BB';
});
describe('小世界', function () {
console.log(`小世界 - ${value}`);
it('小世界のテスト', function () {
console.log(`小世界の中 - ${value}`);
});
})
value = 'C'
console.log(`B-Cの中間 - ${value}`};
it('テストC', function () {
console.log(`テストCの中 - ${value}`);
value = 'CC';
});
console.log(`A-Cの後 - ${value}`);
});
console.log(`世界の中間 - ${value}`);
describe('実行順の確認D-E', function () {
console.log(`D-Eの始まり - ${value}`);
value = 'D';
it('テストD', function () {
console.log(`テストCの中 - ${value}`);
value = 'DD';
});
console.log(`D-Eの中間 - ${value}`);
value = 'E';
it('テストE', function () {
console.log(`テストDの中身 - ${value}`);
value = 'EE';
});
console.log(`D-Eの後 - ${value}`);
});
console.log(`世界の終わり - ${value}`);
この実行結果は下記のようになる。
$ mocha test/test.js
世界の始まり - !!
A-Cの始まり - !!
A-Bの中間 - A
小世界 - B
B-Cの中間 - C
A-Cの後 - C
世界の中間 - C
D-Eの始まり - C
D-Eの中間 - D
D-Eの後 - E
世界の終わり - E
実行順の確認A-C
テストAの中 - E
√ テストA
テストBの中 - AA
√ テストB
テストCの中 - BB
√ テストC
小世界
小世界の中 - CC
√ 小世界のテスト
実行順の確認D-E
テストCの中 - CC
√ テストD
テストDの中身 - DD
√ テストE
6 passing (7ms)
コールバックのコンテキスト
describe
やit
のコールバックが呼び出された時のコンテキスト(this
の指すもの)は、呼び出し元であるMochaが設定したものになる。ただしコールバックをアロー関数として定義すると、コンテキストは、コールバックが定義されたdescribe
やit
のコンテキストとなり、Mochaの設定するコンテキストとはならない。
これは例えば下記のようなコードをit
のコールバック中に書いた時に問題となる。
it('テストの説明', () => {
this.timeout(3000);
// ...
});
timeout()
はMochaの提供する設定用の関数で、this
にMochaのコンテキストが設定されていないと呼び出せない。このためMochaのサイトでは、下記のように書かれている。
Passing arrow functions (aka “lambdas”) to Mocha is discouraged.
ただしdescribe
やit
をクラスのメソッド内に書く場合は、コールバックをアロー関数として定義する。クラスのメソッドでは、そのオブジェクトがコンテキストとして必要だからである。この場合はtimeout()
のような、Mochaの機能を呼び出すことはできない。
テストの打ち切り
複数のit
があり、そのうちの1つのit
でAssertされた後、テストを打ち切りたい場合は、bail
オプションを指定する。
mocha --bail
またはソースコードの中で直接設定する。
this.bail(true);
あるdescribe
の中でit
がAssert
された時に、それ以降のit
の実行を打ち切って、次のdescribe
に実行を移したい場合は、下記のような書き方ができる。この場合、打ち切りではなく、スキップとして記録される。
describe(`テストケース1`, function () {
let failed = false;
beforeEach(function () { if (failed) { this.skip(); } })
afterEach(function () { if (!failed) { failed = this.currentTest?.state === "failed"; } });
it('ステップ1' function() {assert.fail('エラー');});
it('ステップ2', ... );
}
describe(`テストケース2`, function () {
let failed = false;
beforeEach(function () { if (failed) { this.skip(); } })
afterEach(function () { if (!failed) { failed = this.currentTest?.state === "failed"; } });
it( ... );
it( ... );
}
上記の例では、テストケース1のステップ1がエラーとなり、ステップ2はスキップされ、テストケース2は最初から実行される。
非同期処理
テストの中で非同期処理が必要な場合は、it
のコールバックをasyncとして定義する。
describe('テスト', function() {
it('内容', async function() {
await doSomethingAsync();
});
});
モジュールのインポートは要らない
Mochaを使ったテストコードには、Mochaをモジュールとしてインポートする必要はない。Mochaはテストコードを読み込んで起動する際に、Nodeのグローバル環境 (?) に、describeやitを読み込むので、そこから参照できる (らしい)。
ではTypeScriptはどのようにしてdescribe
やit
の存在を知るのか?
TypeScriptは、モジュールのインポート先に型定義のファイルがあればそれを読み込む。モジュールのインポートが無い場合も、一定の規則にしたがって型定義を探しに行く。Mochaの型定義については、@types/mochaから型定義を読み込んでいる。
Chai
Mochaと一緒に使われるChaiについても簡単に …
Chaiとは何か
Chaiはテストの中で使われるAssertionライブラリ。
Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.
https://www.chaijs.com/
Assertionとは、何らかの条件をテストし、結果が偽(エラー)となる場合は、その結果を通知(Assert)するもの。Chaiの場合は、通知のために例外を送出する。(Mochaのit
はこの例外をキャッチする。)
const result = doSomething();
assert.equal(result, "Well done", "Result was not good");
Assertされると、例外が送出されるので、テストコードの実行はそこで打ち切られる。it
の中に記述した場合は、そのit
ブロックの実行が打ち切られ、その結果がit
のテスト結果として記録される。
スタイル
ChaiはMochaと同じく、テストコードの中にテスト仕様を記述できるように意図されている。そのために幾つかのスタイルを用意している。
- TDD向け
- Assert
- BDD向け
- Should
- Expect
Should
とExpect
は、チェーン記法をサポートしているため、テスト条件をより自然言語に近い形で記述できる。
expect(foo).to.have.lengthOf(3);
その他
Mochaはテストドライバを書くためのフレームワークという理解だが、他のライブラリと組み合わせることで、テストスタブを追加したり、機能間の呼び出しをスパイしたりといった手法を適用できるらしい。
こちらはあちこちで見かける … SimonJS
終わりに
Mochaのフレームワークに従うテストコードの構造についての知見をまとめた。書かれたテストの実行順は、見た目の直感とは異なる部分がある。そこを認識していないと、思わぬところで足をすくわれるかも知れない。少なくとも筆者はそうだったし、それが備忘録を残す動機でもある。
もっとも筆者の場合は、独自に書いたテストコードを、後からMocha対応にしたことから、余計な混乱が生じた。Mochaの想定する構造とは異なる構造のなかにMochaを入れようとしたからで、結局全体をバッサリ書き直すことになった。
最初から、”Getting Started”に出てくるようなシンプルな形でテストコードを書けば、すんなり使えてしまうものなのかも知れない。
変更履歴
日付 | 内容 |
2021/10/27 | テストスクリプトの起動コマンドの間違いを修正 npm start -> npm test |
2021/06/10 | VS Codeでのデバックについて追記 |
2021/06/06 | 打ち切りでbailがdescribe単位で有効としていた記述を削除 (全体に有効) describeの中で途中でテストを打ち切る方法について追記 モジュールのインポートについて追記 |
2021/05/28 | 再構成 |
2021/05/25 | コールバックに関する記述改良 |
2021/05/23 | 初版 |
コメント