ツール | |

MochaとChaiでなんでもテスト ~基本編~

MochaとChaiでなんでもテスト 2本目です。

今回は、前回インストールしたMochaとChaiを動かしてみます。

[ Windows 10 / Node.js 10.15.3 / mocha@6.1.4 / chai@4.2.0 ]


  1. その前に
  2. テストプログラムを作る
  3. テストプログラムを起動する
  4. 色々なアサーション
  5. 以上

その前に

MochaとChaiの書き方にはBDDとTDDというのがあるらしいですが、筆者はよく知らずに、適当な方法でずっと書いてしまってます。

以下の記事を読んでみたところ、たぶん今の私の書き方はBDDです。describe とか使いつつ、expect で検証してます。でも前は assert 使ってました。なので、このブログに載ってる書き方はあくまで一例です。

では、次項から始めます。

テストプログラムを作る

早速テストプログラムを作りたいのですが、テスト対象のモジュールが無いとテストできません。

ここではテスト対象として、怪しい関数を用意しておきました。プロジェクトフォルダの直下に mymodule.js ファイルを作成し、中身を以下の通りにしてください。

mymodule.js
/**
 * 引数の絶対値の和を返す。整数以外を渡すとTypeError
 * @param  {Array} nums
 * @return {Number} 絶対値の和
 */
const sumAbs = (...nums) => {
	let res = 0;
	for (let i = 1, len = nums.length; i < len; ++i) {
		res += Math.abs(nums[i]);
	}
	return res;
};

module.exports = { sumAbs };

上記モジュールの sumAbs() のテストプログラムを、試しに作っていきます。


プロジェクト内に test フォルダを作成し、その中に first.js ファイルを作成します。first.js の中身は以下の通りにします。

test/first.js
const expect = require('chai').expect;
const sumAbs = require('../mymodule').sumAbs;

describe('正常系', function() {
	it('0, 1, 2', function() {
		expect(sumAbs(0, 1, 2)).to.equal(3, 'sumAbsの戻り値は3');
	});

	it('0, -1, -2', function() {
		expect(sumAbs(0, -1, -2)).to.equal(3, 'sumAbsの戻り値は3');
	});

	it('1, 2, -3', function() {
		expect(sumAbs(1, 2, -3)).to.equal(6, 'sumAbsの戻り値は6');
	});
});

これだけ見ると「describe とか it はどこから出てきたの?」と思いますが、これらは mocha の機能です。後で mocha を使って起動するので、その時になればちゃんと動きます。

このテストプログラムは、sumAbs() について以下の3ケースをテストするものです。

  1. 「0, 1, 2」を渡すと「3」が返却される。
  2. 「0, -1, -2」を渡すと「3」が返却される。
  3. 「1, 2, 3」を渡すと「6」が返却される。

describe はテストの大項目に当たります。it は小項目です。これらの第一引数に渡した文字列はテスト結果のレポート表示で使われるので、どんな内容か分かるように書きます。

一方、expect から始まる一連の英文みたいな何かは chai の機能です。これは見たまんまで、expect() に渡した実行結果が、後続の英文の通りであることを検証するためのものです。検証がNGとなれば chai.AssertionError を吐き、次の it() に移る仕組みになっています。

ここでは equal() を使って、expect() に渡した実行結果が equal() に渡した値と等価 (===) であることを検証しました。equal() の第二引数は省略可能ですが、書いておくとエラー発生時にテスト結果のレポート表示で使われます。

これでお試しのテストプログラムができました。一旦動かしてみます。

テストプログラムを起動する

作成したテストプログラムを起動して、結果を見ていきます。

プロジェクトフォルダの直下で以下コマンドを叩くことで、mochatest/first.js を実行して、その結果をレポート表示してくれます。

コマンド
.\node_modules\.bin\mocha .\test\first.js
実行結果
  正常系
    √ 0, 1, 2
    √ 0, -1, -2
    1) 1, 2, -3


  2 passing (31ms)
  1 failing

  1) 正常系
       1, 2, -3:

      sumAbsの戻り値は6
      + expected - actual

      -5
      +6

      at Context.<anonymous> (test\first.js:14:31)

ここでは上記のような結果となりました。(実際には色つきで表示されます。)

2 passing 1 failingということで、2ケースは通ったけど、1ケースだけ通りませんでした。3ケース目の「1, 2, -3」で、sumAbs() の戻り値は6を想定していましたが、実際には5が返されたせいです。

原因はもちろん sumAbs()for でループカウンタを1から開始してるからですね。8行目の let i = 1let i = 0 に修正してから、もう一度テストプログラムを起動してみると、以下のようにエラー無しでテストが終了すると思います。

  正常系
    √ 0, 1, 2
    √ 0, -1, -2
    √ 1, 2, -3


  3 passing (9ms)

このように、describeit でテストケースを表現し、except で検証 (アサーション) していくのが基本となります。

色々なアサーション

上で作成したサンプルでは equal() を使いましたが、他にも chai には色々な値を検証できる機能が用意されてます。詳しくはChaiのリファレンスを参照してください。

リファレンスの『Language Chains』に書いてある通り、サンプルに出てきた to は、テストプログラムを英文として読みやすくするためだけのものです。

to 自体に特に機能は無いので、実は expect(sumAbs(1, 2)).equal(3) のように、to を省略しても動きます。

以下にいくつか except を使用したアサーションの例を挙げますが、ここでは『Language Chains』を付けたものの他に、省略したものも少し掲載しておきます。英語が苦手な人は、書かない方が分かりやすいかもしれません。私も苦手です。

.equal()

except() の値と === で比較した結果が false ならばNG。逆は .not.equal()

=== による比較なので、中身が完全に同じ配列やオブジェクトでも、インスタンスが違えばエラーとなる。

expect(1).to.equal(1);      // OK
expect(1).to.not.equal(1);  // NG

expect(1).to.equal('1');      // NG
expect(1).to.not.equal('1');  // OK

expect([2,3]).to.equal([2,3]);  // NG
const arr = [2,3];
expect(arr).to.equal(arr);  // OK
Language Chains 省略
expect(1).equal('1');      // OK
expect(1).not.equal('1');  // OK

.deep.equal()

中身が完全に同じでなければNG。

普通の .equal() と異なり、こちらは値で比較する。小さい配列とかオブジェクトの比較ならこれが楽。

expect([2, 3]).to.deep.equal([2, 3]);  // OK
expect([2, 3]).to.deep.equal([2, 4]);  // NG
expect({ a: 1, b: 2, c: 3 }).to.deep.equal({ a: 1, b: 2, c: 3 });  // OK
expect({ a: 1, b: 2, c: 3 }).to.deep.equal({ a: 1, b: 2, c: 4 });  // NG
expect({ a: 1, b: 2, c: 3 }).to.deep.equal({ a: 1, b: 2, x: 3 });  // NG
expect(new Map([['a', 1], ['b', 2]])).to.deep.equal(new Map([['a', 1], ['b', 2]]));  // OK
expect(new Map([['a', 1], ['b', 2]])).to.deep.equal(new Map([['a', 1], ['b', 3]]));  // NG
Language Chains 省略
expect([2, 3]).deep.equal([2, 3]);  // OK

.true

=== true で比較した結果が false ならばNG。逆は .not.true

1などの true として評価される値でもパスさせたい場合は .ok を使う。

関数ではないので注意! これ系は他に .null, .false, .undefined, .NaN がある。

expect(1).to.be.true;         // NG
expect(true).to.be.true;      // OK
expect(true).to.not.be.true;  // NG

expect(1).to.be.ok;      // OK
expect(0).to.be.ok;      // NG
expect(0).to.not.be.ok;  // OK
Language Chains 省略
expect(true).true;   // OK
expect(1).not.true;  // OK

.match()

正規表現にマッチしなければNG。逆は .not.match()

expect('1111-11').to.match(/^\d{4}-\d{2}$/);      // OK
expect('1111-11').to.match(/^\d{4}-\d{3}$/);      // NG
expect('1111-11').to.not.match(/^\d{4}-\d{3}$/);  // OK
Language Chains 省略
expect('1111-11').match(/^\d{4}-\d{2}$/);      // OK
expect('1111-11').not.match(/^\d{4}-\d{3}$/);  // OK

.lengthOf()

文字列の長さ、配列、Map などの長さが異なればNG。逆は .not.lengthOf()

length プロパティを使ってる関係で、サロゲートペア文字は2として扱われるので注意。

expect([0, 1, 2]).to.have.lengthOf(3);      // OK
expect([0, 1, 2]).to.have.lengthOf(2);      // NG
expect([0, 1, 2]).to.not.have.lengthOf(2);  // OK

expect('abcde').to.have.lengthOf(5);  // OK

expect('🍅!').to.have.lengthOf(3);  // OK

expect({ a: 1, b: 2 }).to.have.lengthOf(2);  // NG (lengthプロパティが無いから)
Language Chains 省略
expect([0, 1, 2]).lengthOf(3);      // OK
expect([0, 1, 2]).not.lengthOf(2);  // OK

.all.keys()

オブジェクトのキーに過不足があればNG。逆は .not.all.keys()

対象のキーはバラバラに渡しても、配列で渡しても大丈夫。

これ系は何種類かあって、.any.keys() なら「指定されたキーをどれか一つでも持ってれば」、.include.all.keys() なら「全てのキーを含んでいれば」となる。保持しているキーが全て把握できてるならこれが一番分かりやすい。

expect({ a: 1, b: 2, c: 3 }).to.have.all.keys('a', 'c');            // NG
expect({ a: 1, b: 2, c: 3 }).to.have.all.keys('a', 'b', 'c');       // OK
expect({ a: 1, b: 2, c: 3 }).to.have.all.keys('a', 'b', 'c', 'd');  // NG
expect({ a: 1, b: 2, c: 3 }).to.not.have.all.keys('a', 'c');        // OK

expect({ a: 1, b: 2, c: 3 }).to.have.all.keys(['a', 'b', 'c']);     // OK
Language Chains 省略
expect({ a: 1, b: 2, c: 3 }).all.keys('a', 'b', 'c');  // OK
expect({ a: 1, b: 2, c: 3 }).not.all.keys('a', 'c');   // OK

.satisfy()

渡した関数が true として評価される値を返さなければNG。逆は not.satisfy()

関数には引数で expect() の値が渡される。なので自由な条件で検証できる。

expect(1).to.satisfy(value => value > 0);       // OK
expect(-1).to.satisfy(value => value > 0);      // NG
expect(-1).to.not.satisfy(value => value > 0);  // OK

.throw()

expect() に関数を渡した場合に使用可能。関数が例外を吐いたらNGとする。逆は .not.throw()

.throw() の第一引数にエラー型を、第二引数にエラーメッセージを渡して検証することもできる。

expect(() => { throw new TypeError('あ!') }).to.throw();      // OK
expect(() => { throw new TypeError('あ!') }).to.not.throw();  // NG

expect(() => { throw new TypeError('あ!') }).to.throw(TypeError);   // OK
expect(() => { throw new TypeError('あ!') }).to.throw(RangeError);  // NG

expect(() => { throw new TypeError('あ!') }).to.throw(TypeError, 'あ!');  // OK
expect(() => { throw new TypeError('あ!') }).to.throw(TypeError, 'あ?');  // NG
expect(() => { throw new TypeError('あ!') }).to.throw(TypeError, /あ/);    // OK

以上

以上で基本編は終わりです。次回はたぶんWebAPI編です。

[2019-05-09 追記] 終わりませんでした。基本編その2書きました。