-
src/testフォルダを作ります。このフォルダはアプリケーションのフォルダ構造を反映していて、src/test/clientフォルダも同様に作ります(serverフォルダとsharedフォルダも必要なら作っても構わないのですが、ここではテストを書きません)。 -
src/test/clientフォルダ内にstate-test.jsファイルを作ります。これはReduxアプリケーションのライフサイクルをテストするのに使われます。
メインのテスティングフレームワークには、Mochaを使うことにします。Mochaは使いやすく、機能が豊富で、今のところ最もよく使われるJavaScriptのテスティングフレームワークです。また、フレキシブルでモジュラー化されています。特に、好きなアサーションライブラリを使うことができます。Chaiは、たくさんのpluginを持つ素晴らしいアサーションライブラリで、異なるアサーションスタイルを選ぶことができます。
yarn add --dev mocha chaiを実行して、MochaとChaiをインストールします。
state-test.jsを以下のように書きます:
/* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */
import { createStore } from 'redux';
import { combineReducers } from 'redux-immutable';
import { should } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import dogReducer from '../../client/reducers/dog-reducer';
import { makeBark } from '../../client/actions/dog-actions';
should();
let store;
describe('App State', () => {
describe('Dog', () => {
beforeEach(() => {
store = createStore(combineReducers({
dog: dogReducer,
}));
});
describe('makeBark', () => {
it('should make hasBarked go from false to true', () => {
store.getState().getIn(['dog', 'hasBarked']).should.be.false;
store.dispatch(makeBark());
store.getState().getIn(['dog', 'hasBarked']).should.be.true;
});
});
});
});さて、この全体を解析してみましょう。
まず、chaiのshouldアサーションスタイルをどのようにimportしているのかを注意してみてください。これを使うとmynumber.should.equal(3)といった、ちょっと巧妙な構文を使ってアサートできるようになります。どんなオブジェクトに対してもshouldを呼び出せるように、何よりも前にshould()を実行しなければなりません。これらのアサーションには、 mybook.should.be.trueのように*式(expressions)*であるものがあり、ESLintはこのような書き方に警告を出します。そのため、ファイルの先頭にno-unused-expressionsルールを無効にするESLintのコメントを追加しています。
Mochaのテストは木構造のような階層構造で動作します。ここでは、アプリケーションの状態のdog属性に影響するmakeBark関数をテストしたいので、テストの階層はdescribe()で表現した通り、App State > Dog > makeBarkという形になります。it()が実際のテスト関数で、beforeEach()はit()の各テストの前に実行される関数になります。この例では、各テストが走る前に新しい状態が必要です。そこで、ファイルの先頭で変数storeを宣言し、このファイル内での全てのテストで使えるようにしています。
makeBarkテストは明示的に書かれていますが、it()内に文字列で与えられている説明によってさらにわかりやすくなっています: ここではmakeBarkの呼び出しによってhasBarkedがfalseからtrueに変わるのをテストしています。
さあ、テストを実行してみましょう!
gulp-mochaプラグインを使って、gulpfile.babel.jsに以下のtestタスクを作ります:
import mocha from 'gulp-mocha';
const paths = {
// [...]
allLibTests: 'lib/test/**/*.js',
};
// [...]
gulp.task('test', ['build'], () =>
gulp.src(paths.allLibTests)
.pipe(mocha())
);- もちろん、
yarn add --dev gulp-mochaで実行します。
このように、テストはlibにトランスパイルされたコードを実行します。これがtestがbuildの前提条件になっている理由です。buildもlintという前提条件を持っており、そして最後に、testがmainの前提条件になります。これにより、defaultは次のようなタスクの連鎖ができます: lint > build > test > main。
mainの前提条件をtestに変えます:
gulp.task('main', ['test'], () => /* ... */ );-
package.jsonの"test"スクリプトを"test": "gulp test"に変更します。こうするとyarn testでテストを実行できるようになります。testは例えばCIサービスのようなツールで自動的に呼ばれる標準のスクリプトでもあるため、必ずテストタスクはここに書くべきです。yarn startはWebpackのクライアントバンドルを作る前にテストを実行するため、全てのテストにパスすればビルドするだけになります。 -
yarn testまたはyarn startを実行すると、テスト結果が出力されます。おそらくグリーンになっているはずです。
ユニットテストでfakeを使いたくなることがあります。たとえば、deleteDatabases()という関数の呼び出しを含むdeleteEverythingという関数があったとします。deleteDatabases()の実行には様々な副作用があるため、テストスイートを走らせる際には実行したくありません。
Sinonは スタブ (とその他もろもろ)を提供するテスティングライブラリで、deleteDatabasesを無効化し実際に呼び出すことなく監視のみ行うようになります。これにより、呼ばれたかどうか、どのような引数で呼ばれたかどうかといったことをテストできるようになります。これはAJAXの呼び出しをフェイクしたり避けたりする場合によく使われます - バックエンド上の副作用を起こしうるような場合です。
ここでは、アプリに対して、src/shared/dog.jsにあるDogクラスのメソッドbarkInConsoleを追加してみます。
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `Wah wah, I am ${this.name}`;
}
barkInConsole() {
/* eslint-disable no-console */
console.log(this.bark());
/* eslint-enable no-console */
}
}
export default Dog;ユニットテストでbarkInConsoleを実行すると、 console.log() はターミナルへの出力を行います。このようなことは、ユニットテストの文脈における望ましくない副作用であると考えられます。興味があるのはconsole.log()が 正常に呼び出されるかどうか であり、テストしたいのはどのようなパラメーターが呼び出されるのかといったことです。
- 新しいファイル
src/test/shared/dog-test.jsを作り、次のように書き加えます:
/* eslint-disable import/no-extraneous-dependencies, no-console */
import chai from 'chai';
import { stub } from 'sinon';
import sinonChai from 'sinon-chai';
import { describe, it } from 'mocha';
import Dog from '../../shared/dog';
chai.should();
chai.use(sinonChai);
describe('Shared', () => {
describe('Dog', () => {
describe('barkInConsole', () => {
it('should print a bark string with its name', () => {
stub(console, 'log');
new Dog('Test Toby').barkInConsole();
console.log.should.have.been.calledWith('Wah wah, I am Test Toby');
console.log.restore();
});
});
});
});ここでは、Sinonのstubsと、その上でChaiのアサーションを使うためのChaiプラグインをimportしています。
yarn add --dev sinon sinon-chaiを実行してライブラリをインストールします。
ここでの新しいことは何でしょうか? 何よりもまず、Chaiプラグインを使うためにchai.use(sinonChai)を呼び出しています。そして、it()文で全ての魔法が発動しています: stub(console, 'log')がconsole.logを無効化し監視します。new Dog('Test Toby').barkInConsole()が実行されると、新たなconsole.logが用意されます。そしてconsole.logをconsole.log.should.have.been.calledWith()で呼び出してテストし、最後にrestoreでconsole.logを元に戻しています。
重要な注意: console.logをスタブするのはおすすめできません。もしテストが失敗すると、console.log.restore()が呼び出されず、そのためconsole.logはターミナルで残りのコマンドをテストしている間中ずっと壊れてしまったままだからです! テストが失敗した時のエラーメッセージも出力できないため、何が起こっているかの情報をほとんど残すことができなくなります。これは大変厄介です。この例は単純なアプリでスタブを説明するためだけのものです。
もしこの章の内容がうまくいっていれば、パスするテストが2つ得られるはずです。
(原文: 11 - Testing with Mocha, Chai, and Sinon)
次章: 12 - Flowによる型検査