先日Mocha でテストの自動化記事を書きましたが、@akameco 氏の記事でava を取り上げていましたので、今度はava を使って同じプログラムをテストしてみることにします。
多々コード等を参考にしている部分がありますので、@akameco 氏にはこの場を借りてお礼を申し上げます。ありがとうございます<(_ _)>
ava で始めるnode のテスト自動化
ava の特徴としては主に以下のようなものがあります(他にもあります)。
* 各ファイルが別プロセスとしてtest される(他のtest ケースの環境変更が他のtest ケースに影響を及ぼさない)
* 各テストケースが並行で実行される
* Ecma Script 6 対応
準備
nodeprj/ // <- ワークディレクトリ
index.js // <- テスト対象のプログラム
+--test/
+--test-divided.js // <- divided.js をテストするプログラム
予めディレクトリを作成しておきます。
~$ mkdir -p nodeprj/test
~$ cd nodeprj
npm init を使用してプロジェクトを初期化します。
入力する内容例としては下記のとおりです。
~$ npm init
...
name: (nodeprj)
version: (1.0.0)
description: The program for dividing numbers.
entry point: (index.js)
test command: ava -v
git repository:
keywords: divided
author: Tsutomu Nakamura
license: (ISC) MIT
...
ava の導入
ava に関するパッケージをインストールします。
またava の--init
オプションでpackage.json
ファイルを初期化します。
また、--save-dev
オプションを使用し、package.json のdevDependencies に追記していきます。
~$ npm install -g ava
~$ ava --version
0.14.0
~$ ava --init
......
~$ cat package.json
{
"name": "nodeprj",
......
"scripts": {
"test": "ava -v"
},
......
"devDependencies": {
"ava": "^0.14.0"
}
}
後ほどテストで使用するsinon もインストールしておきます。
インストールするときはnpm
コマンドのオプションに--save-dev
オプションを使用して、package.json
ファイルに依存関係として追記されるようになります。
~$ npm install --save-dev sinon
......
~$ cat package.json
......
"devDependencies": {
"ava": "^0.14.0",
"sinon": "^1.17.3"
}
}
テストの概要
本セクションでは、次のようなテスト方法について、実際の簡単なプログラムをテストすることで説明していきます。
- 同期的な処理のテスト(基本的なテスト)
- 例外がスローされるかどうかの処理のテスト
- 非同期な処理のテスト
- hook を使ったテストコードのリファクタリング
同期的な処理のテスト(基本的なテスト)
test/test-divided.js
ファイルを新規に作成し、以下のプログラムを作成します。
import test from 'ava';
import divided from '../index.js';
test('divided#calculate returns 2 when the value is 4', t => {
t.is(divided.calculate(4), 2);
});
それではテストを実行してみましょう。
ava は引数にファイルを指定しない場合、デフォルトでtest.js
, test-*.js
, test/**/*.js
な条件にマッチするファイルを検索してテストコードとして実行してくれるので、今回の場合はtest/test-divided.js
が呼びだされます。
~$ npm test
> nodeprj@1.0.0 test /path/to/PracticeMyNodeUnit/ava/nodeprj
> ava -v
module.js:338
throw err;
^
Error: Cannot find module '../index.js'
at Function.Module._resolveFilename (module.js:336:15)
......
✖ test/test-divided.js exited with a non-zero exit code: 1
0 tests passed
1 uncaught exception
npm ERR! Test failed. See above for more details.
すると上記のようにindex.js
が見つからないため、テストがエラーとなって終了しました。
それではindex.js
を作成し、calculate
メソッドを実装し、再度テストを実行してみましょう。
'use strict'
exports.calculate = function(num) {
return num / 2;
};
~$ npm test
> nodeprj@1.0.0 test /path/to/PracticeMyNodeUnit/ava/nodeprj
> ava -v
✔ divided#calculate returns 2 when the value is 4
1 test passed
テストをパスしました。
それでは次に、数値が割り切れなかった場合に小数点以下が切り捨てられるかどうかを確認するテストケースを追加し、テストしてみましょう。
import test from 'ava';
import divided from '../index.js';
test('divided#calculate returns 2 when the value is 4', t => {
t.is(divided.calculate(4), 2);
});
test('divided#calculate returns 1 when the value is 3', t => {
t.is(divided.calculate(3), 1);
});
~$ npm test
想定している結果が1 であるのに1.5 が返ってきているためエラーとなっています。
エラーの内容に従ってプログラムの内容を修正し、再テストします。
'use strict'
exports.calculate = function(num) {
return Math.floor(num / 2);
};
~$ npm test
-w, --watch オプションでファイルの変更が発生したことを契機にテストを自動実行する
ava のテストコマンドに-w, --watch
オプションをつけると、テストコマンドが待ち受け状態に入り、テストコードやテスト対象ファイルに変更が発生したタイミングで自動的にテストを実行してくれるようになります。
また、npm test
コマンドではpackage.json
ファイルに記載されたテストコマンドとオプションで実行するようになっていますが、そのコマンドに追加でオプションを指定したい場合はnpm test -- <追加オプション>
という形式で指定します。
~$ npm test -- --watch
✔ divided › divided#calculate returns 2 when the value is 4
✔ divided › divided#calculate returns 1 when the value is 3
2 tests passed
## 変更が発生すれば自動的にテストを再実行
テストの関数の記述スタイルについて
ava はES6 なスタイルで記述することができるので、先ほど書いたテストケースは次のように書くこともできます。
test('divided#calculate returns 2 when the value is 4', function(t) {
t.is(divided.calculate(4), 2);
});
また、function 名を明示的に定義するようにすれば、そのfunction 名が自動的にテストケースのタイトルとして走るようになっています。
test(function divided#calculate_returns_2_when_the_value_is_4(t) {
t.is(divided.calculate(4), 2);
});
また、テストケースのタイトルを省略することも可能です。
test(function (t) {
t.is(divided.calculate(4), 2);
});
test(t => {
t.is(divided.calculate(4), 2);
});
例外を判定するテストを作成する
プログラムに数値以外の値が引数に渡された時に例外を投げることを確認するテストケースを作成します。
例外が投げられることを確認するにはthrows
function を使用します。
import test from 'ava';
import divided from '../index.js';
test('divided#calculate returns 2 when the value is 4', t => {
t.is(divided.calculate(4), 2);
});
test('divided#calculate returns 1 when the value is 3', t => {
t.is(divided.calculate(3), 1);
});
test('divided#calculate throws exceptions when the value is other than numbers', t => {
t.throws(divided.calculate);
t.throws(() => divided.calculate(null), 'Type of numeric is expected.');
t.throws(() => divided.calculate('abc'), / numeric /);
t.throws(() => divided.calculate([]), TypeError, /^Type of numeric /);
});
$ npm test
......
✔ divided#calculate returns 2 when the value is 4
✔ divided#calculate returns 1 when the value is 3
✖ divided#calculate throws exceptions when the value is other than numbers Missing expected exception..
1 test failed
1. divided#calculate throws exceptions when the value is other than numbers
AssertionError: Missing expected exception..
......
npm ERR! Test failed. See above for more details.
例外が投げられていないため、テストに失敗しました。
プログラムに例外をスローするテストケースを追加し、再テストします。
'use strict'
exports.calculate = function(num) {
if (typeof num !== 'number' || isNaN(num)) {
throw new TypeError('Type of numeric is expected.');
}
return Math.floor(num / 2);
};
$ npm test
......
✔ divided#calculate returns 2 when the value is 4
✔ divided#calculate returns 1 when the value is 3
✔ divided#calculate throws exceptions when the value is other than numbers
3 tests passed
非同期処理のテストをする
非同期で標準入力から値を読み込み、その値の計算結果を出力するプログラムのテストを行います。
EventEmitter, sinon を使用してイベントを非同期に呼び込む処理をエミュレートし、テストを行います。
import test from 'ava';
import sinon from 'sinon';
import {EventEmitter} from 'events';
import divided from '../index.js';
test('divided#calculate returns 2 when the value is 4', t => {
t.is(divided.calculate(4), 2);
});
test('divided#calculate returns 1 when the value is 3', t => {
t.is(divided.calculate(3), 1);
});
test('divided#calculate throws exceptions when the value is other than numbers', t => {
t.throws(divided.calculate);
t.throws(() => divided.calculate(null), 'Type of numeric is expected.');
t.throws(() => divided.calculate('abc'), / numeric /);
t.throws(() => divided.calculate([]), TypeError, /^Type of numeric /);
});
test('divided#read prints "result: 4" when the value is 8 that given from the stdin', t => {
var ev = new EventEmitter();
var _console_log = console.log;
sinon.stub(console, 'log');
process.openStdin = sinon.stub().returns(ev);
divided.read();
ev.emit('data', '8');
t.true(console.log.calledOnce);
t.true(console.log.calledWith('result: 4'));
console.log = _console_log;
});
~$ npm test
......
✔ divided#calculate returns 2 when the value is 4
✔ divided#calculate returns 1 when the value is 3
✔ divided#calculate throws exceptions when the value is other than numbers
✖ divided#read prints "result: 4" when the value is 8 that given from the stdin failed with "undefined is not a function"
1 test failed
1. divided#read prints "result: 4" when the value is 8 that given from the stdin
TypeError: undefined is not a function
......
npm ERR! Test failed. See above for more details.
read
function が未定義のためテストに失敗しました。
read
を追加し、再テストしてみましょう。
'use strict'
exports.calculate = function(num) {
if (typeof num !== 'number' || isNaN(num)) {
throw new TypeError('Type of numeric is expected.');
}
return Math.floor(num / 2);
};
exports.read = function() {
var stdin = process.openStdin();
stdin.on('data', function(chunk) {
var param = Number(chunk);
var result = exports.calculate(param);
console.log('result: ' + result);
});
};
$ npm test
......
✔ divided#calculate returns 2 when the value is 4
✔ divided#calculate returns 1 when the value is 3
✔ divided#calculate throws exceptions when the value is other than numbers
✔ divided#read prints "result: 4" when the value is 8 that given from the stdin
4 tests passed
例外を扱う非同期処理をテストする
非同期で標準入力から値を読み込み、その値の計算結果を出力するfunction に数値以外の値が渡された時に例外が投げられることを確認する処理を追加します。
今回は例外がスローされて'Type of numeric is expected.'
が表示されることを想定しています。
import test from 'ava';
import sinon from 'sinon';
import {EventEmitter} from 'events';
import divided from '../index.js';
test('divided#calculate returns 2 when the value is 4', t => {
t.is(divided.calculate(4), 2);
});
test('divided#calculate returns 1 when the value is 3', t => {
t.is(divided.calculate(3), 1);
});
test('divided#calculate throws exceptions when the value is other than numbers', t => {
t.throws(divided.calculate);
t.throws(() => divided.calculate(null), 'Type of numeric is expected.');
t.throws(() => divided.calculate('abc'), / numeric /);
t.throws(() => divided.calculate([]), TypeError, /^Type of numeric /);
});
test.serial('divided#read prints "result: 4" when the value is 8 that given from the stdin', t => {
var ev = new EventEmitter();
var _console_log = console.log;
sinon.stub(console, 'log');
process.openStdin = sinon.stub().returns(ev);
divided.read();
ev.emit('data', '8');
t.true(console.log.calledOnce);
t.true(console.log.calledWith('result: 4'));
console.log = _console_log;
});
test.serial('divided#read prints "Type of numeric is expected." when the value is not a numeric', t => {
var ev = new EventEmitter();
var _console_log = console.log;
sinon.stub(console, 'log');
process.openStdin = sinon.stub().returns(ev);
divided.read();
ev.emit('data', 'abc');
t.true(console.log.calledOnce);
t.true(console.log.calledWithMatch('Type of numeric is expected.'));
console.log = _console_log;
});
今回、標準入力から値を受け取って結果を表示するテストケースではconsole.log
をstub として定義し、他のテストケースに影響が内容に元の状態に戻す必要があるため、test.serial
を使用するようにしています。
~$ npm test
......
✔ divided#read should print "result: 4" when the value is 8 that given from the stdin
✖ print "Type of numeric is expected." when the value is not a numeric failed with "Type of numeric is expected."
✔ divided#calculate should return 2 when the value is 4
✔ divided#calculate should return 1 when the value is 3
✔ divided#calculate should throw exceptions when the value is other than numbers
1 test failed
1. print "Type of numeric is expected." when the value is not a numeric
TypeError: Type of numeric is expected.
......
npm ERR! Test failed. See above for more details.
例外がスローされなかったためテストに失敗しています。
標準入力から値を受け取って結果を表示する処理に、数値以外の値が渡された時に例外をスローする処理を追加し、再テストしてみましょう。
'use strict'
exports.calculate = function(num) {
if (typeof num !== 'number' || isNaN(num)) {
throw new TypeError('Type of numeric is expected.');
}
return Math.floor(num / 2);
};
exports.read = function() {
var stdin = process.openStdin();
stdin.on('data', function(chunk) {
var param = Number(chunk);
try {
var result = exports.calculate(param);
console.log('result: ' + result);
} catch(e) {
console.log(e.toString());
}
});
};
~$ npm test
......
✔ divided#read prints "result: 4" when the value is 8 that given from the stdin
✔ divided#read prints "Type of numeric is expected." when the value is not a numeric
✔ divided#calculate returns 2 when the value is 4
✔ divided#calculate returns 1 when the value is 3
✔ divided#calculate throws exceptions when the value is other than numbers
5 tests passed
test.serial によるテストケースの実行順について
デフォルトでava は各テストケースを並行(concurently) に実行するようになっていますが、test.serial
を使用することによって、通常の(test.serial
じゃない)テストケースよりも先にこれら(test.serial
な)テストケースをシーケンシャルに実行するようになります。
そのため、広域な場所に定義されている変数に対して変更が発生し、並行実行されることにより他のテストケースに影響を与え、失敗に終わるようなものがあったとしても安全に実行することができます。
// (3) concurent
test('divided#calculate returns 2 when the value is 4', t => { ... });
// (3) concurent
test('divided#calculate returns 1 when the value is 3', t => { ... });
// (3) concurent
test('divided#calculate throws exceptions when the value is other than numbers', t => { ... });
// (1) serial
test.serial('divided#read prints "result: 4" when the value is 8 that given from the stdin', t => { ... });
// (2) serial
test.serial('divided#read prints "Type of numeric is expected." when the value is not a numeric', t => { ... });
hooks を使ってリファクタリングをする
hooks を使用してテストケース毎に行われる処理を共通化することができます。
ava で使用できるhooks は以下のリンクの通りです。
- Before & after hooks
今回のテストではbeforeEach
, afterEach
を使用して各テストケースが実行される前と後でconsole.log
のバックアップとリストアを行っています。
次の例ではt.context
を使いbeforeEach
, afterEach
間でデータを共有できるようにしています(before
, after
のhook では使用できません)。
import test from 'ava';
import sinon from 'sinon';
import {EventEmitter} from 'events';
import divided from '../index.js';
test.beforeEach(t => {
t.context.console_log = console.log;
});
test.afterEach(t => {
console.log = t.context.console_log;
});
test('divided#calculate returns 2 when the value is 4', t => {
t.is(divided.calculate(4), 2);
});
test('divided#calculate returns 1 when the value is 3', t => {
t.is(divided.calculate(3), 1);
});
test('divided#calculate throws exceptions when the value is other than numbers', t => {
t.throws(divided.calculate);
t.throws(() => divided.calculate(null), 'Type of numeric is expected.');
t.throws(() => divided.calculate('abc'), / numeric /);
t.throws(() => divided.calculate([]), TypeError, /^Type of numeric /);
});
test.serial('divided#read prints "result: 4" when the value is 8 that given from the stdin', t => {
var ev = new EventEmitter();
sinon.stub(console, 'log');
process.openStdin = sinon.stub().returns(ev);
divided.read();
ev.emit('data', '8');
t.true(console.log.calledOnce);
t.true(console.log.calledWith('result: 4'));
});
test.serial('divided#read prints "Type of numeric is expected." when the value is not a numeric', t => {
var ev = new EventEmitter();
sinon.stub(console, 'log');
process.openStdin = sinon.stub().returns(ev);
divided.read();
ev.emit('data', 'abc');
t.true(console.log.calledOnce);
t.true(console.log.calledWithMatch('Type of numeric is expected.'));
});
~$ npm test
......
✔ divided#read prints "result: 4" when the value is 8 that given from the stdin
✔ divided#read prints "Type of numeric is expected." when the value is not a numeric
✔ divided#calculate returns 2 when the value is 4
✔ divided#calculate returns 1 when the value is 3
✔ divided#calculate throws exceptions when the value is other than numbers
5 tests passed
特定のタイトルのテストケースだけ実行したい場合
テスト駆動系な開発をしていると、開発作業中は時間節約のため、特定のタイトルのテストのみ実行してほしい場合があります。
ava では--match=
オプションを付け加えることで、指定したタイトルにマッチしたテストのみを実行することができます。
~$ npm test -- --match='divided#calculate*'
~$ ava -v --match='divided#calculate*'
~$ npm test -- --match='!divided#read*'
~$ ava -v --match='!divided#read*'
参考
-
ava, sinonを使ったnode.jsのテスト
-
node.jsのCLIのテストをavaで書いてみる
-
ava github
-
ava-docs ja_JP
-
Mocha, Chai, Sinon を使ったNode.js のテスト自動化 実践編