MochaとChaiでなんでもテスト ~基本編~
MochaとChaiでなんでもテスト 2本目です。
今回は、前回インストールしたMochaとChaiを動かしてみます。
その前に
MochaとChaiの書き方にはBDDとTDDというのがあるらしいです。けれど筆者はよく知らずに、適当な方法でずっと書いてしまってます。
そして以下の記事を読んでみたところ、たぶん今の私の書き方はBDDのようです。describe
とか使いつつ、expect
で検証してます。なのでこの記事に載っている書き方は、BDDのものになります。
Mochaを使ってJavaScriptのテストをブラウザで実行してみよう (1/3):CodeZine(コードジン)
Node.jsでmochaを使ってBDDスタイルのユニットテスト - Qiita
という風に、Mochaは書き方がいくつかあるツールなので、この記事の書き方はあくまで一例だと思ってください。
では、次項から始めます。
テストプログラムを作る
早速テストプログラムを作りたいのですが、テスト対象のモジュールが無いとテストできません。
ここではテスト対象として、怪しい関数を用意しておきました。プロジェクトフォルダの直下に 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
の中身は以下の通りにします。
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ケースをテストするものです。
- 「0, 1, 2」を渡すと「3」が返却される。
- 「0, -1, -2」を渡すと「3」が返却される。
- 「1, 2, 3」を渡すと「6」が返却される。
describe
はテストの大項目に当たります。it
は小項目です。これらの第一引数に渡した文字列はテスト結果のレポート表示で使われるので、どんな内容か分かるように書きます。
一方、expect
から始まる一連の英文みたいな何かは chai
の機能です。これは見たまんまで、expect()
に渡した実行結果が、後続の英文の通りであることを検証するためのものです。検証がNGとなれば chai.AssertionError
を吐き、次の it()
に移る仕組みになっています。
ここでは equal()
を使って、expect()
に渡した実行結果が equal()
に渡した値と等価 (===
) であることを検証しました。equal()
の第二引数は省略可能ですが、書いておくとエラー発生時にテスト結果のレポート表示で使われます。
これでお試しのテストプログラムができました。一旦動かしてみます。
テストプログラムを起動する
作成したテストプログラムを起動して、結果を見ていきます。
プロジェクトフォルダの直下で以下コマンドを叩くことで、mocha
が tests/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 = 1
を let i = 0
に修正してから、もう一度テストプログラムを起動してみると、以下のようにエラー無しでテストが終了すると思います。
正常系
√ 0, 1, 2
√ 0, -1, -2
√ 1, 2, -3
3 passing (9ms)
このように、describe
と it
でテストケースを表現し、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
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
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
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
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プロパティが無いから)
expect([0, 1, 2]).lengthOf(3); // OK
expect([0, 1, 2]).not.lengthOf(2); // OK
.above() / .least()
expect()
の値が .above()
を超過していればパス。
expect()
の値が .least()
以上ならパス。least = 少なくとも。
逆は .not.above()
, .not.least()
だが、それはつまり後述の .most()
, .below()
と同じ。
expect(3).to.be.above(2); // OK
expect(3).to.be.above(3); // NG
expect(3).to.not.be.above(2); // NG
expect(3).to.not.be.above(3); // OK
expect(3).to.be.at.least(3); // OK
expect(3).to.be.at.least(4); // NG
expect(3).to.not.be.at.least(3); // NG
expect(3).to.not.be.at.least(4); // OK
expect(3).above(2); // OK
expect(3).not.above(3); // OK
expect(3).least(3); // OK
expect(3).not.least(4); // OK
.below() / .most()
expect()
の値が .below()
未満ならパス。
expect()
の値が .most()
以下ならパス。most = 大きくても。
逆は .not.below()
, .not.most()
だが、それはつまり前述の .least()
, .above()
と同じ。
expect(3).to.be.below(3); // NG
expect(3).to.be.below(4); // OK
expect(3).to.not.be.below(3); // OK
expect(3).to.not.be.below(4); // NG
expect(3).to.be.at.most(2); // NG
expect(3).to.be.at.most(3); // OK
expect(3).to.not.be.at.most(2); // OK
expect(3).to.not.be.at.most(3); // NG
expect(3).below(4); // OK
expect(3).not.below(3); // OK
expect(3).most(3); // OK
expect(3).not.most(2); // OK
.include()
配列に指定した値が含まれていればパス。.includes()
でも可。逆は .not.to.include()
。
値は一つだけ渡す。オブジェクトも検証できるらしいけど、オブジェクトにこれを使ったことがないのでよく分からない。
配列を順不同で検証したい場合、想定される値を全て .include()
で検証した後 .lengthOf()
で長さを検証すればよい……と思っているのだが、もっと良い方法があるかも。
expect([1, 2]).to.include(1); // OK
expect([1, 2]).to.include(2); // OK
expect([1, 2]).to.include(3); // NG
expect([1, 2]).to.not.include(3); // OK
expect([1, 2]).include(1); // OK
expect([1, 2]).not.include(3); // 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
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
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
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の機能を紹介します。