目次
はじめに
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
はそのテストコードをMochaに登録する。Mochaは後述する実行順序でテストコードを呼び出す。
テストコードの中ではテストを実行しその結果を検証し、エラーがあれば例外を送出する。Mochaはその例外をキャッチして、テスト結果としてレポートする。次に記述するdescribe
と違い、ネストすることはできない。
describe
describe
は、複数のit
をまとめられる。また階層化(ネスト)してテストを構造化できる。
コールバックの実行順序
describe
とit
の本体は、コールバックとして記述されることに注意する。つまりdescribe
やit
の呼び出しとコールバックの呼び出しは異なるタイミングとなる。
describe
とit
で、コールバックの実行順序は下記のようになる。
describe
のコールバックは、そのdescribe
の中で実行されるdescribe
がネストしている場合は、describe
のコールバックの実行もネストする (親->子->親)
it
のコールバックは、全てのdescribe
のコールバックが処理されたあとに実行される- 同じ
describe
の中にあるit
のコールバックは、定義されている順番に実行される describe
がネストしている場合は、親のレベルのit
のコールバックが全て実行されたあとに、子のレベルのit
のコールバックが実行されるdescribe
のネストとは実行順が異なる
コールバックのコンテキスト
it
のコールバックが呼び出される時、そのコンテキストはMochaが設定したものとなる。そのコンテキストには、describe
やit
などのMocha自身の機能が予めロードされている。(初めてMochaのプログラム例を見た時、describe
やit
がどこから来たのか不思議だったが …)
describe
のコールバックに、it
の呼び出し以外の何らかのコードを書くことは禁止されていない。ただし、その結果がどのようにテストコード (it
のコールバック) に反映されるのかは要注意である。
コールバックの実行順序とコンテキストを確認するために、下記のようなプログラムを書いてみた。途中変数value
に値を入れ直しながら、コールバックの中で参照している。
console.log(`世界の始まり`);
let value = '!!';
describe('世界#1', function () {
console.log(`> 世界#1の始まり - ${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 = 'SS';
});
console.log(`>> 小世界の終わり - ${value}`);
})
console.log(`> B-Cの中間 - ${value}`);
value = 'C'
it('テストC', function () {
console.log(`テストCの中 - ${value}`);
value = 'CC';
});
console.log(`> 世界#1の終わり - ${value}`);
});
console.log(`世界の中間 - ${value}`);
describe('世界#2', function () {
console.log(`> 世界#2の始まり - ${value}`);
value = 'D';
it('テストD', function () {
console.log(`テストDの中 - ${value}`);
value = 'DD';
});
console.log(`> D-Eの中間 - ${value}`);
value = 'E';
it('テストE', function () {
console.log(`テストEの中身 - ${value}`);
value = 'EE';
});
console.log(`> 世界#2の終わり - ${value}`);
});
console.log(`世界の終わり - ${value}`);
この実行結果は下記のようになる。
世界の始まり
> 世界#1の始まり - !!
> A-Bの中間 - A
>> 小世界の始まり - B
>> 小世界の終わり - B
> B-Cの中間 - B
> 世界#1の終わり - C
世界の中間 - C
> 世界#2の始まり - C
> D-Eの中間 - D
> 世界#2の終わり - E
世界の終わり - E
世界#1
テストAの中 - E
✔ テストA
テストBの中 - AA
✔ テストB
テストCの中 - BB
✔ テストC
小世界
小世界の中 - CC
✔ 小世界のテスト
世界#2
テストDの中 - SS
✔ テストD
テストEの中身 - DD
✔ テストE
6 passing (2ms)
describe
とit
でコールバックの実行タイミングが異なることが分かる。また各it
の中で参照している変数value
の値は、直前に外側のdescribe
の中で定義されたものとは異なっている。(グローバル変数を参照しているので、value
の更新はコールバックの実行順に更新・引継されることになる。)
前処理と後処理のためのHook
describe
には、before
/after
/beforeEach
/afterEach
といったHookを設定できる。Hookはユーザーの用意する関数で、ここではテストの前処理、後処理を記述できる。
before
/after
はdescribe
でまとめられたテスト全体の前処理、後処理であり、beforeEach
/afterEach
は、各テスト対する前処理、後処理となる。
describe('Before/After', function () {
before(function () {
console.log('> Before');
});
beforeEach(function () {
console.log('> BeforeEach');
});
after(function () {
console.log('> After');
});
afterEach(function () {
console.log('> AfterEach');
});
describe('Before/After - Sub', function () {
it('Test - Sub', function () {
console.log('>> Test - Sub');
});
});
it('Test#1', function () {
console.log('> Test#1');
});
it('Test#2', function () {
console.log('> Test #2');
})
});
この実行結果を下記に示す。
Before/After
> Before
> BeforeEach
> Test#1
✔ Test#1
> AfterEach
> BeforeEach
> Test #2
✔ Test#2
> AfterEach
Before/After - Sub
> BeforeEach
>> Test - Sub
✔ Test - Sub
> AfterEach
> After
3 passing (3ms)
beforeEach
/afterEach
は、そのdescribe
内の全てのit
で共通となる。it
ごとに前処理、後処理の内容を変えたい時はどうするのか? beforeEach
/afterEach
の中で工夫できるかも知れないが、素直に1つのdescribe
に1つのit
を割り当てるほうが良さそうである。
アロー関数の利用は推奨されない
it
のコールバックは、function
を使って定義する。一般的にはfunction
の代わりにアロー関数を使っても同じ結果が得られる。しかしit
の場合は結果が異なる。Mochaがit
を呼び出す時には、独自のコンテキストを設定して呼び出すからである。しかしコールバックをアロー関数として定義した場合、コンテキストは、コールバックが定義されたit
のコンテキストとなり、Mochaの設定するコンテキストとはならない。
これは例えば下記のようなコードをit
のコールバック中に書いた時に問題となる。
it('テストの説明', () => {
this.timeout(3000);
// ...
});
timeout()
はMochaの提供する設定用の関数で、this
にMochaのコンテキストが設定されていないと呼び出せない。このためMochaのサイトでは、下記のように書かれている。
Passing arrow functions (aka “lambdas”) to Mocha is discouraged.
https://mochajs.org/#arrow-functions
テストの打ち切り
複数の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はテストコードを読み込んで起動する際に、事前に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 assert = require('chai').assert;
describe('Test with Chai', function () {
it('Test', function () {
// Do something ...
const result = false;
// ...
assert.equal(result, true, 'asserted');
});
});
Test with Chai
1) Test
0 passing (2ms)
1 failing
1) Test with Chai
Test:
asserted
+ expected - actual
-false
+true
at Context.<anonymous> (test/test-with-chai.js:6:16)
at process.processImmediate (node:internal/timers:476:21)
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”に出てくるようなシンプルな形でテストコードを書けば、すんなり使えてしまうものなのかも知れない。
変更履歴
日付 | 内容 |
2023/09/12 | コールバックとコンテキストに関する説明を整理・加筆 |
2023/09/10 | describeとitの説明を見直し |
2023/09/08 | 実行サンプルのシンタックスエラーとラベル間違いを修正 |
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 | 初版 |