Unit test
nodejs の単体テストライブラリnodeunit
を使って、単体テスト自動化の初歩について説明していきます。
テストの概要
本セクションでは、次のようなテスト方法について、実際の簡単なプログラムをテストすることで説明していきます。
- 同期的な処理のテスト(基本的なテスト)
- 例外がスローされるかどうかの処理のテスト
- 非同期な処理のテスト
- setUp, tearDown を使ったテストコードのリファクタリング
nodeunit のインストール
nodeunit
をインストールします。
$ sudo npm install -g nodeunit
インストールが完了したら、nodeunit
コマンドを実行して問題なく実行されることを確認してみましょう。
$ nodeunit -v
0.9.1
もしコマンドが見つからない場合、nodeunit コマンドに対してPATH を通すなりを各自行ってください(環境依存な部分なので、詳細な手順は省略します)。
作業ディレクトリの作成
今回のディレクトリ/ファイル構成は次のようにします。
nodeprj/ // <- ワークディレクトリ
+--lib/
| +--divided.js // <- テスト対象のプログラム
+--test/
+--test-divided.js // <- divided.js をテストするプログラム
これらのディレクトリとファイルは事前に作成するようにしておいてください。
$ mkdir -p nodeprj
$ cd nodeprj
$ mkdir {lib,test}
今回作成するのは、与えられた数値を2 で割って、小数点以下を切り捨てた物を計算するプログラムをテスト駆動型(TDD)で開発していく仮定を例に、nodeunit
の使い方について説明していきたいと思います。
同期的な処理のテスト
test/test-divided.js
ファイルを新規作成し、以下のテストプログラムを記述します。
テストプログラムを記述するときは、モジュール内で共有されるexports
オブジェクトにメソッドを代入していく形式でやっていきます。
test.equal
はアサーションを行うメソッドで、ここにテストの想定する結果を記載します。
また、各テストケースの終了時にはtest.done
メソッドが呼ばれるようにしてください。
var divided = require('../lib/divided');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.done();
};
プログラムを作成したらnodeunit
コマンドでテストを実行しましょう。
nodeunit
コマンドを実行するときはnodeunit "テストプログラムが入っているディレクトリ"
という形式で実行します。
今回はtest
ディレクトリにテストプログラムが入っているので、nodeunit test
とコマンドを実行します。
~nodeprj$ nodeunit test
module.js:340
throw err;
^
Error: Cannot find module '../lib/divided'
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
...
at Module.require (module.js:364:17)
結果を見るとモジュールの呼び出しに失敗していることがわかります。
このテスト結果を参考に、まずはlib/divided.js
ファイルを作成し、calculate
メソッドを実装してみましょう。
/** 与えられた値を2 で割って小数点以下を切り捨てた結果を返す関数 */
exports.calculate = function(num) {
};
上記のファイルを作成したら、再度nodeunit
を実行します。
すると、想定した結果が2 であるのに対し、undefined
が返ってきているためエラーとなっています。
もう一度divided.js
ファイルを開き処理を追加してみましょう。
/** 与えられた値を2 で割って小数点以下を切り捨てた結果を返す関数 */
exports.calculate = function(num) {
return num / 2;
};
テストを実行してみます。
$ nodeunite test
テストに成功しました。それでは次のテストとして、計算結果が割り切れなかった場合に小数点以下を切り捨てられるかどうかのテストを追加してみます。
var divided = require('../lib/divided');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.done();
};
nodeunit test
を実行します。
$ nodeunit test
test-divided
✖ calculate
AssertionError: 1.5 == 1
at Object.equal (/usr/local/lib/node_modules/nodeunit/lib/types.js:83:39)
at Object.exports.calculate (/opt/nodeprj/test/test-divided.js:5:10)
......
at _concat (/usr/local/lib/node_modules/nodeunit/deps/async.js:512:9)
FAILURES: 1/2 assertions failed (7ms)
すると上記のように、1 が返ることを想定していたところ、1.5 が返ってきたためエラーとなっています。
上記結果を参考に、プログラムに小数点以下を切り捨てる処理を追加します。
/** 与えられた値を2 で割って小数点以下を切り捨てた結果を返す関数 */
exports.calculate = function(num) {
return Math.floor(num / 2);
};
nodeunit test
を実行します。
~nodeprj$ nodeunit test
test-divided
✔ calculate
OK: 2 assertions (5ms)
これで小数点以下を切り捨てる処理の実装は完了です。
例外を判定するテストを作成する(数値じゃないものが渡された時に例外を投げる処理のテスト/実装)
数値以外の値がメソッドの引数に渡されたら例外を投げる処理のテスト/実装を行います。
例外がスローされるかどうかの試験をする場合はtest.throws
メソッドの引数に例外をスローするメソッドを渡して実行します。
var divided = require('../lib/divided');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
$ nodeunit test
test-divided
✖ calculate
AssertionError: undefined "Missing expected exception.."
at _throws (/usr/local/lib/node_modules/nodeunit/lib/assert.js:329:5)
at assert.throws (/usr/local/lib/node_modules/nodeunit/lib/assert.js:346:11)
...
at iterate (/usr/local/lib/node_modules/nodeunit/deps/async.js:123:13)
AssertionError: undefined "Missing expected exception.."
at _throws (/usr/local/lib/node_modules/nodeunit/lib/assert.js:329:5)
at assert.throws (/usr/local/lib/node_modules/nodeunit/lib/assert.js:346:11)
...
FAILURES: 4/6 assertions failed (26ms)
想定した例外がスローされていないためテストが失敗しています。
lib/divided.js
に数値以外の値が指定されたら例外をスローする処理を追加します。
/** 与えられた値を2 で割って小数点以下を切り捨てた結果を返す関数 */
exports.calculate = function(num) {
if (typeof num !== 'number') {
throw new Error('Type of numeric is expected.');
}
return Math.floor(num / 2);
};
例外をスローする処理を追加したら、テストを実行します。
$ nodeunit test
test-divided
✔ calculate
OK: 6 assertions (5ms)
成功しました。
非同期処理のテストをする(標準入力から値を受け取ってコンソールに出力する処理のテスト/実装)
非同期で標準入力から値を読み込むプログラムをテストします。
今回は標準入力から値を受け取り、計算結果をconsole.log
で結果をコンソールに出力するプログラムのテストケースを追記します。
まずは今までと同じようにテストケースread a number
を追加してみましょう。
標準入力から渡された数値を計算し、その結果をconsole.log
で標準出力するという処理ということにしておきます。
var divided = require('../lib/divided');
var events = require('events');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
exports['read a number'] = function(test) {
var ev = new events.EventEmitter();
process.openStdin = function() { return ev; };
process.exit = test.done;
var _console_log = console.log;
console.log = function(str) {
console.log = _console_log;
test.equal(str, 'result: 4');
};
divided.read();
ev.emit('data', '8');
};
上記のサンプルコードでは、EventEmitter を使用してdata
イベントを発生させることにより、標準入力をエミュレートするものです。
divided には、この後記載しますが、標準入力から与えられた8 を2 で割って標準出力にresult: 4
と出力されることを想定した内容になっています。
標準出力へはconsole.log
を使って出力するので、console.log メソッドをstub なメソッドで上書きし、stub の中で想定した通りの値がconsole.log の引数として渡されたかどうかをアサートしています。
また、今回は標準出力でconsole.log
をstub で上書きしているので、テストを実施したらconsole.log
を元の状態に戻しておきます。
そのため今回は、一旦console.log
メソッドの参照を_console_log
変数に退避しておき、stub 内で元に戻すようにしています。
var _console_log = console.log;
console.log = function(str) {
console.log = _console_log;
...
それではテストを実行してみましょう。
~nodeprj$ nodeunit test
test-divided
✔ calculate
TypeError: Object #<Object> has no method 'read'
at Object.exports.read a number (/path/to/nodeprj/test/test-divided.js:25:13)
at Object.<anonymous> (/usr/local/lib/node_modules/nodeunit/lib/core.js:236:16)
at /usr/local/lib/node_modules/nodeunit/lib/core.js:236:16
......
lib/divided.js
のread
メソッドが定義されていないためテストに失敗しました。
lib/divided.js
にread
メソッドを追加していきましょう。
/** 与えられた値を2 で割って小数点以下を切り捨てた結果を返す関数 */
exports.calculate = function(num) {
if (typeof num !== 'number') {
throw new Error('Type of numeric is expected.');
}
return Math.floor(num / 2);
};
/** 標準入力から値を受け取り、計算結果を返すメソッド */
exports.read = function() {
var stdin = process.openStdin();
stdin.on('data', function(chunk) {
var param = parseFloat(chunk);
var result = exports.calculate(param);
console.log('result: ' + result);
process.exit();
});
};
テストを実行します。
$ nodeunit test
test-divided
✔ calculate
✔ read a number
OK: 7 assertions (6ms)
テストに成功しました。
……が、今回このテストは不完全な状態です。
今回たまたま全部のassertions 7個("calculate" 6 個、"read a number" 1 個) が実行され、結果がOK となりましたが、"read a number" でテストをしているread
メソッドは非同期で実行されるため、場合によっては全部で6 個しか実行されないでテストが終了してしまう可能性があります(今回の実行環境では再現させることができませんでした)。しかもそれはテスト結果としてOK となってしまいます。
もし非同期で実行されなかったテストケースがNG 出会った場合、テストでその障害を発見できない可能性が出てきてしまいます。
それを解消するためにtest.expect
メソッドを使用して、事前に"read a number テストケースには1 つのassertion があるよ!" ということをテストケースに通知することができるようになっています
テストケースに対して幾つのassertion があるかを通知するにはexpect
メソッドを使います。
では、実際にテストプログラム側を修正してみましょう。
以下のテストプログラムはテストケース開始時にtest.expect
メソッドで1 個のassertion があるということを事前に通知し、現在のテストケースで幾つのassertion があるかをnodeunit に伝えることができます。
var divided = require('../lib/divided');
var events = require('events');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
exports['read a number'] = function(test) {
test.expect(1);
var ev = new events.EventEmitter();
process.openStdin = function() { return ev; };
process.exit = test.done;
var _console_log = console.log;
console.log = function(str) {
console.log = _console_log;
test.equal(str, 'result: 4');
};
divided.read();
ev.emit('data', '8');
};
nodeunit test
を実行してみましょう。
$ nodeunit test
test-divided
✔ calculate
✔ read a number
OK: 7 assertions (19ms)
上記のようにOK: 7 assertions
と表示されれば、しっかりと7 箇所のassertion が実行されたうえでOK になっているということが確認できます。
assertion の数がtest.expect で指定した数より少なかった場合
今回、test.expect
メソッドを使ってこのテストケースがassertion を1 つ持つということを通知していましたが、もしtest.expect
で指定した数とassertion の数が異なっていた場合、どのようになるかを擬似的に確認してみましょう。
上記のテストケースのうち、テストプログラムのtest.expect
をコメントアウトし、test.equal
もコメントアウトし、テストを実行してみましょう。
var divided = require('../lib/divided');
var events = require('events');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
exports['read a number'] = function(test) {
// test.expect(1);
var ev = new events.EventEmitter();
process.openStdin = function() { return ev; };
process.exit = test.done;
var _console_log = console.log;
console.log = function(str) {
console.log = _console_log;
// test.equal(str, 'result: 4');
};
divided.read();
ev.emit('data', '8');
};
$ nodeunit test
test-divided
✔ calculate
✔ read a number
OK: 6 assertions (21ms)
すると、calculate
, read a number
のテストケースが実行され、全体でassertion が6 件実行され、テスト結果OK が返ってきています。
が、実際このテストではread a number
テストケースに1 つもassertion が入っていないのにテストがOK になっているということになります。
それでは次にtext.expect
のコメントアウトはそのままで、test.equal
のコメントを解除してテストを実行すると、どのような動きになるか確認してみましょう。
......
exports['read a number'] = function(test) {
// test.expect(1);
var ev = new events.EventEmitter();
......
console.log = function(str) {
console.log = _console_log;
test.equal(str, 'result: 4'); // <-- コメントアウトを解除
};
......
};
$ nodeunit test
test-divided
✔ calculate
✔ read a number
OK: 7 assertions (6ms)
すると、7 個のassertion が実行されて結果がOK となりました。
これは、test.equal
のassertion が実行される/されないといった事はテストをpass するかどうかに関係が無く、この項目が非同期処理によって実行されなかったとしても見た目上はテストOK となってしまう危険性をはらんでいます。
この危険性を回避するためにtest.expect
を使って想定した件数のassertion が実行されなかった場合にNG とするようにします。
それではそれを確認するためにtest.expect(1);
の部分のコメントアウトを解除して、test.equals
の部分をコメントアウトしてテストケースを実行してみましょう。
......
exports['read a number'] = function(test) {
test.expect(1); // <-- コメントアウトを解除
var ev = new events.EventEmitter();
......
console.log = function(str) {
console.log = _console_log;
// test.equal(str, 'result: 4'); // <-- コメントアウト
};
......
};
$ nodeunit test
test-divided
✔ calculate
✖ read a number
Error: Expected 1 assertions, 0 ran
at process.test.done [as exit] (/usr/local/lib/node_modules/nodeunit/lib/types.js:121:25)
at EventEmitter.<anonymous> (/opt/nodeprj/lib/divided.js:16:17)
......
FAILURES: 1/7 assertions failed (8ms)
✖ read a number
......
すると、1 個のassertion が実行されることが期待されているが、0 個のassertion が実行されたというエラーが出ます。
このようにして非同期処理によってassertion が実行されなかった場合でも、テストケースをNG として検知することができるようになります。
複数のテストケースで共有される変数について
次は複数テストケースを追加していった時に共有されるデータについて注意する点について説明していきたいと思います。
複数のテストケース間で共有されるデータについて認識していないままテストケースを作成してしまうと想定しない動きとなることがあります。
そのことについて確認するために、次のような、標準入力から値を受け取る非同期メソッドが数値以外を受け取った時に例外をスローするケースを追加して説明していきたいと思います。
var divided = require('../lib/divided');
var events = require('events');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
exports['read a value other than a number'] = function(test) {
test.expect(1);
var ev = new events.EventEmitter();
process.openStdin = function() { return ev; };
process.exit = test.done;
divided.calculate = function() {
throw new Error('Expected a number');
};
var _console_log = console.log;
console.log = function(str) {
console.log = _console_log;
test.equal(str, 'Error: Expected a number');
};
divided.read();
ev.emit('data', 'asdf');
};
exports['read a number'] = function(test) {
test.expect(1);
var ev = new events.EventEmitter();
process.openStdin = function() { return ev; };
process.exit = test.done;
var _console_log = console.log;
console.log = function(str) {
console.log = _console_log;
test.equal(str, 'result: 4');
};
divided.read();
ev.emit('data', '8');
};
テスト対象プログラムに例外をスローする処理を追加し、テストを実行します。
/** 与えられた値を2 で割って小数点以下を切り捨てた結果を返す関数 */
exports.calculate = function(num) {
if (typeof num !== 'number') {
throw new Error('Type of numeric is expected.');
}
return Math.floor(num / 2);
};
/** 標準入力から値を受け取り、計算結果を返すメソッド */
exports.read = function() {
var stdin = process.openStdin();
stdin.on('data', function(chunk) {
var param = parseFloat(chunk);
try {
var result = exports.calculate(param);
console.log('result: ' + result);
} catch(e) {
console.log(e);
}
process.exit();
});
};
$ nodeunit test
test-divided
✔ calculate
✔ read a value other than a number
✖ read a number
AssertionError: [Error: Expected a number] == 'result: 4'
at Object.equal (/usr/local/lib/node_modules/nodeunit/lib/types.js:83:39)
at Console.console.log (/opt/nodeprj/test/test-divided.js:41:14)
at EventEmitter.<anonymous> (/opt/nodeprj/lib/divided.js:19:21)
...
FAILURES: 1/8 assertions failed (9ms)
✖ read a number
...
すると、calculate
, read a value other than a number
テストケースについては成功していますが、read a number
テストについては失敗してしまいました。
失敗した原因としては、EventEmitter で数値を標準入力に渡していますが、[Error: Expected a number]
な例外がスローされていることによります。
テストケースread a number
は先ほどまでは成功していたテストケースですが、今回新しいテストケースread a value other than a number
をread a number
テストケースの前に入れることで、失敗するようになってしまいました。
これはread a value other than a number
テストケースでstub を設定している部分がありますが、そのstub が設定されたままread a number
テストケースが実行されることによります。
ここでは各非同期なテストケースが実施された後に、stub を設定した変数を全て元に戻しておくように、process.exit
が呼ばれたタイミングでstub を元に戻す処理を実行するようにしましょう。
var divided = require('../lib/divided');
var events = require('events');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
exports['read a value other than a number'] = function(test) {
test.expect(1);
var ev = new events.EventEmitter();
var _process_openStdin = process.openStdin;
process.openStdin = function() { return ev; };
var _process_exit = process.exit;
process.exit = function() {
// テストケース終了時にstub を元に戻す
process.openStdin = _process_openStdin;
process.exit = _process_exit;
divided.calculate = _divided_calculate;
console.log = _console_log;
test.done();
};
var _divided_calculate = divided.calculate;
divided.calculate = function() {
throw new Error('Expected a number');
};
var _console_log = console.log;
console.log = function(str) {
test.equal(str, 'Error: Expected a number');
};
divided.read();
ev.emit('data', 'asdf');
};
exports['read a number'] = function(test) {
test.expect(1);
var ev = new events.EventEmitter();
var _process_openStdin = process.openStdin;
process.openStdin = function() { return ev; };
var _process_exit = process.exit;
process.exit = function() {
// テストケース終了時にstub を元に戻す
process.openStdin = _process_openStdin;
process.exit = _process_exit;
console.log = _console_log;
test.done();
}
var _console_log = console.log;
console.log = function(str) {
test.equal(str, 'result: 4');
};
divided.read();
ev.emit('data', '8');
};
テストプログラムを書き換えたらnodeunit test
を実行します。
$ nodeunit test
test-divided
✔ calculate
✔ read a value other than a number
✔ read a number
OK: 8 assertions (7ms)
全てのテストケースで成功OK が返ってきました。
また上記のテストケース実行結果を見ると、テストケース内に非同期な処理がある場合でも、各テストケース間はシーケンシャルに実行されていることが確認できると思います。
このようにテストケース間はシーケンシャルであるために、毎回テストケースの値を初期化することで、次のテストケースに影響を及ぼさず、独立したstub 環境でテストを走らせることができるようになります。
もしテストケース間が並行に実行されてしまうようなことがあった場合、テストケースread a value other than a number
がstub を元に戻す前にread a number
が実行されてしまい、正しくテストができない可能性が出てきてしまいます。
テストケースにsetUp, tearDown を実装する
今まで作成してきたテストケースではストケースが開始する時と、各テストケースが終了するときに必要な処理(stub の作成等) をテストケースの処理内部に直接書いていましたが、setUp
, tearDown
メソッドを使用することで、それを1 箇所にまとめて共通化することができます。
setUp
メソッドは各テストケースが実行される前に実行されるメソッドで、tearDown
メソッドは各テストケースが終了するときに実行されるメソッドです。
setUp
, tearDown
を利用するにあたり、nodeunit
ライブラリへのパスを通しておく必要があります。
nodeunit
ライブラリを使用できるようにするために~/.node_libraries
ディレクトリにnodeunit
ライブラリのコピー/シンボリックリンクを作成する、NODE_PATH
環境変数にnodeunit
のライブラリがあるディレクトリを指定する等、事前にセットアップを実施しておいてください。
それではsetUp
, tearDown
メソッドを使ってテストプログラムを作成してみましょう。
setUp
, tearDown
メソッドを実装するには、require('nodeunit');
を最初に定義してnodeunit
を取り入れるようにしてください。
var divided = require('../lib/divided');
var events = require('events');
var nodeunit = require('nodeunit');
exports['calculate'] = function(test) {
test.equal(divided.calculate(4), 2);
test.equal(divided.calculate(3), 1);
test.throws(function() { divided.calculate(); });
test.throws(function() { divided.calculate(null); });
test.throws(function() { divided.calculate("abc"); });
test.throws(function() { divided.calculate([]); });
test.done();
};
exports['read'] = nodeunit.testCase({
setUp: function(callback) {
// 各メソッドの参照を保持しておく
this._process_openStdin = process.openStdin;
this._console_log = console.log;
this._divided_calculate = divided.calculate;
this._process_exit = process.exit;
var ev = this.ev = new events.EventEmitter();
process.openStdin = function() { return ev; };
callback();
},
tearDown: function (callback) {
// 全てのオーバーライドしたメソッドを元に戻す
process.opensStdin = this._process_openStdin;
process.exit = this._process_exit;
divided.calculate = this._divided_calculate;
console.log = this._console_log;
callback();
},
// 数値以外の数値が渡された時のテスト
'a value other than a number': function(test) {
test.expect(1);
process.exit = test.done;
divided.calculate = function() {
throw new Error('Expected a number');
};
console.log = function(str) {
test.equal(str, 'Error: Expected a number');
};
divided.read();
this.ev.emit('data', 'abc');
},
// 数値が渡された時の正常系テスト
'a number': function(test) {
test.expect(1);
process.exit = test.done;
console.log = function(str) {
test.equal(str, 'result: 4');
};
divided.read();
this.ev.emit('data', '8');
}
});
テストプログラムを作成したら、実行してみましょう。
$ NODE_PATH=/usr/local/lib/node_modules nodeunit test
test-divided
✔ calculate
✔ read - a value other than a number
✔ read - a number
OK: 8 assertions (9ms)
上記のようにテストケースをパスすれば成功です。
参考
-
Unit testing in node.js
-
[node.js] Getting started with nodeunit
-
Nodeunit