ツール | |

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. expectを使った色々なアサーションの書き方
  5. 以上

その前に

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

そして以下の記事を読んでみたところ、たぶん今の私の書き方はBDDのようです。describe とか使いつつ、expect で検証してます。なのでこの記事に載っている書き方は、BDDのものになります。

という風に、Mochaは書き方がいくつかあるツールなので、この記事の書き方はあくまで一例だと思ってください。

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

テストプログラムを作る

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

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

mymodule.js
/**
 * 引数の絶対値の和を返す
 * @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() のテストプログラムを、試しに作っていきます。


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

tests/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() の第二引数は省略可能ですが、書いておくとエラー発生時にテスト結果のレポート表示で使われます。

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

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

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

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

コマンド
.\node_modules\.bin\mocha .\tests\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> (tests\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 で検証 (アサーション) していくのが基本となります。

expectを使った色々なアサーションの書き方

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

リファレンスの『Language Chains』に書いてある通り、この記事のサンプルに出てきた to は、テストプログラムを英文として読みやすくするためだけのものです。他にも is とか has とかいろいろ用意されてます。

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

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

.equal()

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

=== による比較なので、中身が完全に同じ配列やオブジェクトでもインスタンスが違えばエラーとなる。配列やオブジェクトの中身を比較するなら、次項の .deep.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()

except() の値と完全に一致すればパス。

普通の .equal() と異なり、こちらは値を比較する。配列やオブジェクトの中身が完全に予測できるなら、これが一番楽。

expect(2).to.deep.equal(2);  // OK
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 Buffer([1, 2])).to.deep.equal(new Buffer([1, 2]));  // OK
expect(new Buffer([1, 2])).to.deep.equal(new Buffer([1, 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 で比較した結果が true ならばパス。逆は .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()

正規表現にマッチすればパス。逆は .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 などの長さが指定値と一致すればパス。逆は .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()

オブジェクトに、指定したキーが過不足なく含まれていればパス。逆は .not.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

.all を省略しても同じように動くが、公式では可読性のため .all を付けることが推奨されている

.include.all.keys()

オブジェクトに、指定したキーが全て含まれていればパス。逆は .not.include.all.keys()

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

オブジェクトのキーを検証するなら .all.keys() を使った方が漏れなく検証できるが、必要なキーさえ揃っていれば他はどうでもいい場合には、こちらが使える。シチュエーションとしては、外部APIが関わるテストか。

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

.all を省略しても同じように動くが、公式では可読性のため .all を付けることが推奨されている

.any.keys()

オブジェクトに、指定したキーが一つでも含まれていればパス。逆は .not.any.keys()

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

あまり使うシチュエーションは無い。.all.keys() が使えない状況で使うにしても、これを使うならまだ .include.all.keys() を使った方が良いと思う。

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

.satisfy()

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

関数には引数で expect() の値が渡される。

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

// OK
expect(1).to.satisfy(function(value) {
	return value > 0;
});

// NG
expect(0).to.satisfy(function(value) {
	return value > 0;
});

.throw()

expect() に渡した関数が例外を吐かなければパス。逆は .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

以上

以上で今回は終わりです。次回は基本編その2で、もっと便利なMochaの機能を紹介します。