Facebook 製 JavaScript テストツール Jest を使ってテストする ( Babel, TypeScript のサンプル付き )
福井祐人
この記事はRECRUIT MARKETING PARTNERS Advent Calendar 2017の投稿記事です。
はじめまして。11月にJoinしたフロントエンドエンジニアの福井(@fukumasuya)です。チームではスタディサプリEnglishのWebブラウザ版の開発を担当しています。
突然ですが皆さんはプロジェクトでJavaScriptのテストをするときにどのツールを使うか迷うことはないでしょうか?フレームワークは?アサーションライブラリは?テストランナーは?テストカバレッジはどうするか?などなど決めないとダメなことが多く苦労していませんか?そんな皆さんにはFacebook製オールインワンテストツールのJest
をオススメします。
この記事ではJest
初心者向けにJavaScript
、ES2015(ES6)
+Flow
、TypeScript
の3パターンで一からプロジェクトを作成し、簡単なテストを書いて動作確認とテストカバレッジを取得する方法を紹介します。まだJest
を触ったことがないかたにとってこの記事が素晴らしいJest
の世界に触れるきっかけになれば幸いです。
参考
動作確認環境
yarn v1.2.1
もしくはnpm v5.5.1
jest v21.2.1
MacOS Sierra(10.12.6)
サンプルコードは GitHub で公開していますので、ぜひ併せてご参照ください。
What is Jest?
冒頭に書いたようにFacebook社が提供しているMITライセンスのオールインワンJavaScriptテストツールです。以下にあるようなテストツールとして求められる機能をはじめから備えているのが特徴で同じFacebook製のReact
と相性がよく、React
を採用しているプロジェクトではよく一緒に利用されています。しかしJest
は決してReact
専用というわけではなくAngular
やVue.js
などReact
以外のプロジェクトでも普通に利用できます。言語に関しても通常のJavaScript以外にもES2015(ES6)
、Flow
、TypeScript
で書かれたコードに対応しています。またJest
にはテストカバレッジを取得できる機能があり、コードのどの部分がテストされていないのかすぐ確認できます。この機能については記事の最後で簡単に紹介します。
Jestが提供する機能(一部)
beforeEach()
、afterAll()
などのSetup、Teardownexpect()
関数が提供する様々なマッチャー- ネイティブ関数や自作関数のモック化
Promise
やAsync/Await
など非同期処理のテスト- UIの変更を検知できるスナップショット
- テストカバレッジ取得
- 参考: Jest Docs
Hello Jest
では早速Jest
をプロジェクトに導入して簡単なテストを実行してみましょう。
セットアップ
セットアップはyarn
もしくはnpm
で行います。適当な名前で空ディレクトリを作成して以下を実行してください。最初のgit init
は必須ではありませんが、やっておくとコード変更を監視して影響するテストファイルだけテストするwatchオプションが使えるので便利になります。
yarn
$ git init
$ yarn init
...(適当に初期値を入力)
$ yarn add -D jest
npm
$ git init
$ npm init
...(適当に初期値を入力)
$ npm install --save-dev jest
テスト対象ファイルの作成
引数に指定した文字列を加工してHelloメッセージを出力する関数のテストをしてみます。プロジェクトにテスト対象ファイル(hello.js
)を追加しましょう。
function hello(name) {
return 'Hello ' + name + '!!';
}
module.exports = hello;
テストファイルの作成
次に作成したhello.js
のhello()
関数をテストするためのhello.test.js
をsrc
ディレクトリに作成します。Jestはデフォルトで*.test.js
、*.spec.js
もしくは__tests__
という名前のディレクトリ以下のファイルをテストファイルとみなします。これはtestRegexというオプションを使うことで変更できます。
テストはtest()
関数の中で実行します。第一引数にテストのDescriptionを記述でき、第二引数にテスト内容を関数として記述します。その関数の中でexpect()
関数にテストしたい値(hello('Jest')
)を引数として渡し、マッチャーtoBe()
で値をチェックします。すべてのテストは基本的にこの流れです。二つ目のテストはnot
を使って結果を逆転させています。マッチャーは他にもたくさんあるので興味があるかたはぜひ調べてみてください。
var hello = require('./hello');
test('hello("jest") to be "Hello Jest!!"', function() {
expect(hello('Jest')).toBe('Hello Jest!!');
});
test('hello("jest") not to be "Hello fukumasuya!!"', function() {
expect(hello('Jest')).not.toBe('Hello fukumasuya!!');
});
プロジェクト構造はこのようになっているはずです。
root/
├ ...
├ package.json
└ src/
├ hello.js
└ hello.test.js
テスト実行
いよいよテスト実行です。まずpackage.json
のscripts
にtest
コマンドを追加してコマンドラインからテストを実行できるようにします。
{
...
"devDependencies": {
"jest": "^21.2.1"
}
...
"scripts": {
"test": "jest"
}
}
ターミナルでyarn test
もしくはnpm run test
を実行します。watch
オプションで実行する場合はyarn test --watch
もしくはnpm run test --watch
を実行してください。
(npm
とyarn
の結果を両方載せると冗長なので今後はyarn
のみ表示します。npm
でも結果は同じです。)
$ yarn test
yarn run v1.2.1
$ jest
PASS src/hello.test.js
✓ hello("jest") to be "Hello Jest!!" (5ms)
✓ hello("jest") not to be "Hello fukumasuya!!"
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.406s
Ran all test suites.
Done in 2.07s.
watch
オプションで実行した場合はこうなります。テストプロセスは終了せず監視モードになっているのでファイルを変更しただけで再テストが必要なファイルは自動的にテストされます。
PASS src/hello.test.js
hello
✓ hello("jest") to be "Hello Jest!!" (2ms)
✓ hello("jest") not to be "Hello fukumasuya!!"
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.892s, estimated 1s
Ran all test suites related to changed files.
Watch Usage
› Press a to run all tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
以上で終了です。どうでしたか?本当にJest
を導入するだけでテストできましたね。
ES2015(ES6) + Flow でテストを書く
我々のチームでは開発言語としてTypeScript
を採用していますが、最近のモダンな開発チームでは通常のJavaScriptではなくES2015(ES6)
、ES2016(ES7)
やFacebook製の静的型チェッカーであるFlow
を採用していることも多いと思います。そういうプロジェクトでももちろんJest
は利用可能です。Jest
にはデフォルトでBabel
のJestプラグインbabel-jestがセットされているため、プロジェクトのBabel
の設定を使ってソースをトランスパイルしてテストできます。
セットアップ
先程テストを実行したプロジェクトをそのまま使います。まずES6
用のBabelプリセットbabel-preset-es2015
、Flow
用のBabelプリセットbabel-preset-flow
とFlow CLIのflow-bin
をインストールします。
package.json の編集
flowコマンドを実行できるよう、package.jsonに以下を追加します
{
...
"devDependencies": {
"babel-preset-es2015": "^6.24.1",
"babel-preset-flow": "^6.23.0",
"flow-bin": "^0.61.0",
"jest": "^21.2.1"
},
...
"scripts": {
"test": "jest",
"flow": "flow"
}
}
続いてFlow
を初期化します。以下を実行するとプロジェクトに.flowconfig
ファイルが追加されます。この記事のテストを動かすだけならこのファイルを編集する必要はありませんのでそのままにしておきます。
.babelrc の編集
次に.babelrc
を編集します。presets
に先程インストールしたes2015
とflow
を設定します。他に使いたいプリセットがあれば同様にインストールして.babelrc
に書くことでJest
も自動的にその設定にしたがってソースを解釈するようになります。
{
"presets": ["es2015", "flow"]
}
以上でセットアップ完了です。
テスト対象ファイルとテストファイルの編集
次にテスト対象ファイルとテストファイルをES6
とFlow
に対応したコードに変更します。まずFlow
の型チェック対象としてファイルを指定するためには// @flow
をファイルの先頭に指定する必要があります。hello()
関数もせっかくなのでアロー関数として定義してみましょう。引数の型はstring
、返り値の型は型チェックがされているか確認するためあえてnumber
としてみましょう。ES6
なのでexport
も使えます。
// @flow
export const hello = (name: string): number => `Hello ${name}!!`;
次にテストファイルです。テストファイルにも// @flow
を追加してFlow
のチェック対象にしてみましょう。hello()
関数をimport
でロードします。それ以外は基本的に同じですが、せっかくなので先程使ったtest()
の代わりにdescribe()
とit()
を使ってみましょう。describe()
を使うと複数のテストをグルーピングして管理することができます。ネストもできるので関数やクラス単位でうまくグルーピングするとテストファイルの見通しがよくなります。it()
はtest()
のエイリアスで中身は同じです。この書き方はRSpec
やJasmine
など他のテストツールと同じなので馴染みがあるかたは多いのではないでしょうか。
// @flow
import { hello } from './hello';
describe('hello', () => {
it('hello("jest") to be "Hello Jest!!"', () => {
expect(hello('Jest')).toBe('Hello Jest!!');
})
it('hello("jest") not to be "Hello fukumasuya!!"', () => {
expect(hello('Jest')).not.toBe('Hello fukumasuya!!');
});
});
試しにyarn flow check
で型チェックを実行してみましょう。
yarn run v1.2.1
$ flow check
Error: src/hello.js:2
2: export const hello = (name: string): number => `Hello ${name}!!`;
^^^^^^^^^^^^^^^^^ string. This type is incompatible with the expected return type of
2: export const hello = (name: string): number => `Hello ${name}!!`;
^^^^^^ number
Error: src/hello.test.js:4
4: describe('hello', () => {
^^^^^^^^ describe. Could not resolve name
Error: src/hello.test.js:5
5: it('hello("jest") to be "Hello Jest!!"', () => {
^^ it. Could not resolve name
Error: src/hello.test.js:6
6: expect(hello('Jest')).toBe('Hello Jest!!');
^^^^^^ expect. Could not resolve name
Error: src/hello.test.js:9
9: expect(hello('Jest')).not.toBe('Hello fukumasuya!!');
^^^^^^ expect. Could not resolve name
Found 5 errors
error Command failed with exit code 2.
すごく怒られましたね…最初のエラーでは予期したとおりhello()
の返り値に指定した型(number
)と実際に返る型(string
)が一致していないというもので、その他のエラーはテストファイルの中で未定義の関数(describe()
、it()
、expect()
)が使われてるというものです。これらのエラーは次のようにすることで解消できます。
hello.js
の返り値の型をstring
にする
// @flow
export const hello = (name: string): string => `Hello ${name}!!`;
Jestの型ファイルをインストールする
テストファイル中の未定義関数エラーはFlow
がJest
の型情報を知らないため起きているので、型ファイルをプロジェクトにインストールしてやります。Jest
の型情報をチェックするためまずflow-typed
をインストールしましょう。
インストール直後はパスが通ってないかもしれないので、ターミナルを再起動してflow-typed search jest
で型情報を調べます。
$ flow-typed search jest
• rebasing flow-typed cache...done.
Found definitions:
╔======╤==================╤=====================╗
║ Name │ Package Version │ Flow Version ║
╟------┼-----------------┼---------------------╢
║ jest │ v21.x.x │ >=v0.39.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v21.x.x │ >=v0.22.x <=v0.38.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v20.x.x │ >=v0.39.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v20.x.x │ >=v0.22.x <=v0.38.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v19.x.x │ >=v0.16.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v18.x.x │ >=v0.16.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v17.x.x │ >=v0.16.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v16.x.x │ >=v0.16.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v14.x.x │ >=v0.16.x ║
╟------┼-----------------┼---------------------╢
║ jest │ v12.0.x │ >=v0.16.x ║
╚======╧==================╧=====================╝
使っているJest v21.2.1
に対応する型ファイルが見つかったのでプロジェクトにインストールします。
$ flow-typed install jest@21.x.x
• Searching for 1 libdefs...
• rebasing flow-typed cache...
• Installing 1 libDefs...
• jest_v21.x.x.js
└> ./flow-typed/npm/jest_v21.x.x.js
jest
これでjest
の型情報ファイルがプロジェクトに入ったので、
再度yarn flow check
を実行してみましょう。
$ yarn flow check
yarn run v1.2.1
$ flow check
Found 0 errors
Done in 2.38s.
Found 0 errors
が表示されたらOKです!
テスト実行
最後にyarn test
でテストを実行します。
$ yarn test
yarn run v1.2.1
$ jest
PASS src/hello.test.js
hello
✓ hello("jest") to be "Hello Jest!!" (2ms)
✓ hello("jest") not to be "Hello fukumasuya!!"
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.751s
Ran all test suites.
Done in 3.66s.
Jest
でES6
とFlow
で書かれたコードがテストできました。おまけですが、describe()
を使っているので2つのテストがhello
グループとして管理されていることがわかると思います。
最終的なプロジェクト構造はこのようになっています。
root/
├ ...
├ package.json
├ .babelrc
├ .flowconfig
├ src/
│ ├ hello.js
│ └ hello.test.js
└ flow-typed/
└ npm/
└ jest_v21.x.x.js
TypeScriptでテストを書く
今度はTypeScript
でテストを書いてみましょう。TypeScript
はMicrosoft製の静的型付け言語でAngular
でも採用されており、プロジェクトで採用しているチームも多いと思います。Jest
でも幾つか設定を加えることでTypeScirpt
のテストができるようになります。
セットアップ
まず、Jest
を入れただけの状態のプロジェクトにtypescript
、Jestの型ファイル@types/jest
、TypeScript
のJestプラグインのts-jest
を導入します。
続いて以下のようにpackage.json
にtsc
コマンドを登録します。
{
...
"devDependencies": {
"@types/jest": "^21.1.8",
"jest": "^21.2.1",
"ts-jest": "^21.2.4",
"typescript": "^2.6.2"
}
...
"scripts": {
"test": "jest",
"tsc": "tsc"
}
}
さらに、yarn tsc --init
を実行してtsconfig.json
ファイルを作成します。
tsconfig.json
が作成されたらファイルを開いて、compilerOptions
のtarget
をes2015
に変更します。es2016
が使いたい場合はそのように変えても構いません。その他はデフォルトのままで大丈夫です。
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
...
}
...
}
次にts-jestのガイドに従ってpackage.json
に以下を追記しましょう。この設定によってJest
実行時に*.test.ts
ファイルがテスト対象となりTypescript
でトランスパイルされるようになります。
{
...
"devDependencies": {
"@types/jest": "^21.1.8",
"jest": "^21.2.1",
"ts-jest": "^21.2.4",
"typescript": "^2.6.2"
}
...
"scripts": {
"test": "jest",
"tsc": "tsc"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}
以上でセットアップ完了です。
テスト対象ファイルとテストファイルの編集
ではテスト対象ファイルとテストファイルを編集しましょう。テスト対象ファイル(hello.ts
)をsrc
ディレクトリに作成します。ソースの中身はES6
版から// @flow
を除いたものです。こちらもせっかくなのでhello()
関数の返り値の型をnumber
にして型チェックしてみましょう。
次にテストファイルです。
こちらもES6
版から// @flow
を除いたものとまったく同じものをsrc
ディレクトリにhello.test.ts
として作成しましょう。
import { hello } from './hello';
describe('hello', () => {
it('hello("jest") to be "Hello Jest!!"', () => {
expect(hello('Jest')).toBe('Hello Jest!!');
})
it('hello("jest") not to be "Hello fukumasuya!!"', () => {
expect(hello('Jest')).not.toBe('Hello fukumasuya!!');
});
});
作成したらyarn tsc
コマンドで型チェックしてみましょう。
$ yarn tsc
yarn run v1.2.1
$ tsc
src/hello.ts(1,48): error TS2322: Type 'string' is not assignable to type 'number'.
error Command failed with exit code 2.
想定したとおり型チェックでエラーになりました。ES6
版と同じようにhello()
関数の返り値の型をstring
に戻して再実行しましょう。
$ yarn tsc
yarn run v1.2.1
$ tsc
Done in 1.30s.
無事成功しました。これでsrc
ディレクトリにトランスパイルされたhello.js
とhello.test.js
が作成されているはずです。興味があればどのように変換されたか覗いてみてください。今回は必要ないので確認が終わればファイルは削除して構いません。
テスト実行
いよいよテスト実行です。yarn test
でテストを実行します。
$ yarn test
yarn run v1.2.1
$ jest
PASS src/hello.test.ts
hello
✓ hello("jest") to be "Hello Jest!!" (2ms)
✓ hello("jest") not to be "Hello fukumasuya!!"
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.441s
Ran all test suites.
Done in 1.86s.
成功したでしょうか?これでTypeScript
もJest
でテストできることがお分かりいただけたと思います。
最終的なプロジェクト構造はこのようになります。型定義ファイルはFlow
と違ってnode_modules
の@types
以下に配置されています。
root/
├ ...
├ package.json
├ tsconfig.json
├ .flowconfig
├ src/
│ ├ hello.ts
│ └ hello.test.ts
└ node_modules/
└ @types/
└ jest/
├ index.d.ts
├ ...
└ ...
テストカバレッジ
もう一つJest
に初めから備わっている機能一つとしてテストコードのカバレッジを測定する機能があります。実際の開発でカバレッジを100%にすることはほぼ不可能ですし、100%にこだわることに意味はありませんが、自分たちのテストコードが対象ファイルをどの程度カバーできているのかを知ることは役に立ちます。ここでは試しに先程作成したテストファイルのカバレッジを測定してみましょう。
まずTypeScript
版のテスト対象ファイルを以下のように編集します。引数のname
がある場合はそれを使ってhelloメッセージを出力し、ない場合はデフォルトのメッセージを出力するよう条件分岐させます。これくらいなら三項演算子で一行で書くこともできますが今回は見やすさみのためにあえて複数行で書いてみます。
export const hello = (name?: string): string => {
if (name) {
return `Hello ${name}!!`;
} else {
return 'Hello Jest!!';
}
}
これで準備完了なので--coverage
オプションつきでテストを実行します。
$ yarn test --coverage
yarn run v1.2.1
$ jest --coverage
PASS src/hello.test.ts
hello
✓ hello("jest") to be "Hello Jest!!" (2ms)
✓ hello("jest") not to be "Hello fukumasuya!!" (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.413s
Ran all test suites.
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 80 | 50 | 100 | 80 | |
hello.ts | 80 | 50 | 100 | 80 | 8 |
----------|----------|----------|----------|----------|----------------|
テスト結果とともにカバレッジも出力されました。中身を見てみるとFuncs
は100%ですが、その他が100%ではありません。特にBranch
が50%ですね。これはどこか辿ってない条件分岐があるということです。Jest
は測定結果をlcov
としても出力してくれるのでそちらで詳細を確認してみましょう。プロジェクトのルートディレクトリ直下にcoverage
ディレクトリが作成されているはずです。その直下のlcov-report
の中にあるindex.html
をブラウザで開いてみましょう。
コマンド出力と同じ情報が表示されています。hello.ts
リンクを開くと各ファイルの詳細が確認できますので開いてみましょう。
4行目にE
マークがついています。これは4行目のelse
ケースに分岐していないことを表しています。テストカバレッジをあげるためテストファイルの2つ目のテストでhello()
を引数なしで呼ぶようテストファイルを編集してみましょう。
import { hello } from './hello';
describe('hello', () => {
it('hello("jest") to be "Hello Jest!!"', () => {
expect(hello('Jest')).toBe('Hello Jest!!');
})
it('hello() to be "Hello Jest!!"', () => {
expect(hello()).toBe('Hello Jest!!');
});
});
もう一度テストを実行してみます。
$ yarn test --coverage
yarn run v1.2.1
$ jest --coverage
PASS src/hello.test.ts
hello
✓ hello("jest") to be "Hello Jest!!" (2ms)
✓ hello() to be "Hello Jest!!"
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.262s
Ran all test suites.
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
Done in 3.35s.
カバレッジが100%になりました!lcovでみてもこの通り100%になっていますね。
カバレッジがこんなに簡単に取得できるとはすごく便利ですね。lcovレポートをチームのサーバーなどでホストして見えるようにするとチームでカバレッジ情報を共有できるのでテストへの意識が高まることと思います。さらにJest
ではcoverageThresholdパラメータをセットすることでカバレッジにしきい値を設定することが可能です。テスト実行時に設定した割合を下回るとテスト失敗と判断されます。
いかがでしたでしょう?この記事ではJest
のほんの一部を紹介しただけですが、魅力的なテストツールだと感じていただけたでしょうか?この記事を参考に皆さんもぜひJest
を触ってみてください!