JavaScript ES6 + JestでTDD(環境構築+基本編)
ブラウザ向けのコードをJavaScript ES6で書きましょう。
コードを書くならユニットテストをしながら書きたいですね。
今回はJavaScriptでTDD環境を整えます。ES6のimportが原因でエラーが出やすいので環境構築部分を詳細に書きました。(長文です)
node.jsのコードをTDDするケースは別記事にしたいと思います。
Jest
JestはJavaScriptのユニットテストフレームワークです。
他にもMochaなど有名なフレームワークがありますが今回はJestを使います。
Babel
ブラウザ上で動かすコードをES6で書いてJestを実行するとエラーが出ます。原因はES6とCommonJSの違いです。
Jestはnode.jsで動きます。node.jsではモジュールの読み込みは「require」で行います。
一方、ES6ではモジュールの読み込みは「import」で行います。この「import」をJestが理解できなくてエラーが出ます。
解決策としてES6 コードをnode.jsが理解できるES5 コードに変換してJestを実行します。しかもJestで試験するときだけ変換を実行します。この変換作業を行うのがBabelです。
node.js
以降の手順は既にnode.jsがインストールされていることが前提です。
まだの人は下記のリンクを参考にしてインストールしてください
MacにNode.jsをインストールする - プログラミング初心者がアーキテクトっぽく語る
環境作成手順
プロジェクトフォルダを作成
- 適当な名前のプロジェクトフォルダを作成します。
mkdir jest-sample
- 作成したらVSCodeでOpen folder...からプロジェクトフォルダを開きます。
package.jsonの雛形作成
- VSCodeのViewメニューからTerminalを選択します。
- Terminalが開くので「npm init」と打ちます。
- 質問には全てEnterでOKです。
- package.jsonが作成されます。
モジュールをインストール
- Babelをインストールします。
npm install --save-dev @babel/core @babel/preset-env
- Jestをインストールします。
npm install --save-dev jest babel-jest @types/jest
--save-devを忘れないでください。
@types/jestも入れておくとVSCodeで補完ができて便利です。
jest.config.jsを作成
- プロジェクトフォルダのトップ階層にjest.config.jsを作成して以下の内容を記述します。
module.exports = { verbose: true , testMatch: [ "**/test/**/*.test.js" ], };
babel.config.jsの作成
- プロジェクトフォルダのトップ階層にbabel.config.jsを作成して以下の内容を記述します。
module.exports = { presets: [ [ '@babel/preset-env', { 'modules': 'false', 'useBuiltIns': 'usage', 'targets': '> 0.25%, not dead', } ] ], env: { test: { presets: [['@babel/preset-env', {targets: {node: 'current'}}]], }, }, };
package.jsonを編集
- package.jsonのscripts > testsを下記の通り「jest」に変更します。
<変更前> "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, <変更後> "scripts": { "test": "jest" }
動作確認
- プロジェクトフォルダにtestフォルダを作成します。
- test/car.test.jsを作成し、以下の内容を記載します。
- 「i」と入力すると「it」が補完候補として表示されることも確認して下さい。
- 補完機能が動かない場合は@types/jestがインストールされていない可能性があります。
describe('nothing', () => { it('should be nothing', () => { }); });
- Terminalで「npm test」を実行します。
$ npm test > jest-sample@1.0.0 test /Users/john/Code/JavaScript/jest-sample > jest PASS test/car.test.js nothing ✓ should be nothing (1 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.471 s Ran all test suites. $
- これで環境は完成です。
コーディング
- 次はTest Firstでコーディングしていきます。内容はIntellijかPyCharmのTutorialに登場したcarクラスをいじってます。
インスタンスを作成するテスト
- 最初のテストを削除してCarクラスをインスタンス化するテストをcar.test.jsに書きます。
- この時点ではなにもAssertしていません。
- 試験は「it」か「test」から始めます。
describe('Car', () => { it('should be created', () => { new Car(); }); });
- テストがFailすることを確認します。
$ npm test > jest-sample@1.0.0 test /Users/john/Code/JavaScript/jest-sample > jest FAIL test/car.test.js Car ✕ should be created (1 ms) ● Car › should be created ReferenceError: Car is not defined 1 | describe('Car', () => { 2 | it('should be created', () => { > 3 | new Car(); | ^ 4 | }); 5 | }); at Object.<anonymous> (test/car.test.js:3:9) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 1.414 s Ran all test suites. npm ERR! Test failed. See above for more details.
Carクラスがないので怒っています。
car.jsを作り空のCarクラスを記述します。
class Car { } export default Car;
- car.test.jsでcar.jsをインポートします。
import Car from '../car'
- テストがパスすることを確認します。
$ npm test > jest-sample@1.0.0 test /Users/john/Code/JavaScript/jest-sample > jest PASS test/car.test.js Car ✓ should be created (1 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.174 s Ran all test suites. $
TDDではこんな感じのTest > Pass > Refactorを繰り返します。
このテストでimport関連のエラーが出る場合はBabel設定が間違っている可能性あります。Babel関係の設定を見直してください。
期待結果と比較する
- 期待結果との比較はexpect().toXXXで行います。
describe('Car', () => { it('should be empty at first', () => { const car = new Car(); expect(car.speed).toBe(0); }); });
- どんなtoXXXが利用可能かはVSCodeで補完機能を使って確かめてください。
Fixtureを使う
試験の最初にいつも実施する作業はbeforeEachにまとめます。
下の例では各テストの中で実施していたインスタンス作成作業をbeforeEachに移しました。
describe('Car', () => { let car; beforeEach(() => { car = new Car(); <<< 移動 }); it('should be empty at first', () => { expect(car.speed).toBe(0); }); it('should have speed of 5 after one acceleration', () => { car.accelerate(); expect(car.speed).toBe(5); }); });
Exceptionを試験する
- ExceptionはtoThrowで確認できます。便利。。。
it('should throw exception if driver is distracted', () => { const action = () => { car.driveByGirls(); }; expect(action).toThrow('Keep your eyes on the road!'); });
非同期の試験(Promise/then)
JavaScriptで多用する非同期処理の試験方法です。
Promiseを返す関数なら単純です。JestはPromiseがresolveされるまで待ってくれます。thenにexpectを書いておけばOKです。
car.jsに以下の関数を追加します。関数はPromiseを返します。
coolDownForGivenTime(time) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(15); }, time * 1000); }) .then((currentTemp) => currentTemp ); }
car.test.jsに以下のテストを追加します。
Promiseが resolveされるのを待って、戻ってきたcurrentTempの値をexpect.toBeでチェックしてます。
it('should cool down the room asynchronousy', () => { return car.coolDownForGivenTime(5).then((temp) => { expect(temp).toBe(15); }) });
非同期の試験(async/await)
- Promiseを返す関数ならasync/awaitを使って試験することもできます。
it('should cool down the room asynchronousy with async/await', async() => { const temp = await car.coolDownForGivenTime(5); expect(temp).toBe(15); });
外部ライブラリをMockする
外部ライブラリをモックする方法を紹介します。ここではradio.jsのRadio.playMusicメソッドをStub化します。
radio.jsの空ファイルを作成します。中身はなくても大丈夫です。
car.jsでradio.jsをimportしてRadio.playMusicを使用します。
import Radio from './radio'; <略> playMusic() { return Radio.playMusic(); }
- car.test.jsでradio.jsをimportしてRadio.playMusicを「La Bamba」を返すStubにしています。
import Radio from '../radio'; <略> it(`should play a music and return the title`, () => { Radio.playMusic = jest.fn().mockReturnValue('La Bamba'); const title = car.playMusic(); expect(title).toBe('La Bamba'); });
fetch APIをテスト
長くなったので別記事にしました。