ブラウザ向けのコードを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をテスト
長くなったので別記事にしました。