JavaScriptを単体テストする流行りのフレームワークJestを試してみる

# Jestとは
JestはJavaScriptの単体テストのフレームワークです。
https://jestjs.io/ja/

テストランナーだけでなく、モック機能やカバレッジの取得を使用することができます。npmのトレンドとしては２０１９年から伸びてmochaを超えるものとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/a6108d11-9853-5e17-9c5c-8be3b6405acc.png)
https://www.npmtrends.com/jest-vs-jasmine-vs-mocha-vs-qunit

この記事は公式のサンプルコードを弄ってその挙動を確認するものとなっています。
また、実験環境は以下の通りです。

OS:MacOS Catalina 10.15.6
node.js: v14.10.1


## 簡単なはじめ方
jestを使用するためにnode.jsのプロジェクトを以下のように作成します。

```
 # package.jsonを作成する
 npm init -y
 # package.jsonにjestを追加してnode_modulesにインストール
 npm install --save-dev jest
```

jestではデフォルトでは&#095;&#095;tests&#095;&#095;フォルダ中のjavascritpをテストコードとして実行するので、&#095;&#095;tests&#095;&#095;フォルダにテストコードを追加します。

```js:__tests__/sample.test.js
test('test 1.', ()=>{
  expect(1+2).toBe(3);
});
```

テストを動かすには以下のコマンドを実行します。

```
# テストを開始する
npx jest
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/ebe2eba7-d1c9-ea26-f2af-c56918c98cfe.png)

## 設定ファイル
jestの設定は以下のいずれかの方法で指定することができます。

 - package.jsonに記載する
 - jest.config.jsに記載する
 - 実行時に--configオプションで指定したファイルに指定する

https://jestjs.io/docs/ja/configuration

### jest.config.jsの作成
下記のコマンドで対話式でjest.config.jsを作成します。

```
npx jest --init
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/c787784e-957f-3cef-08de-76d5c0c18d86.png)

## カバレッジの収集方法
下記のように--coverageオプションを指定して実行することでカバレッジの収集を行います。

```
npx jest --coverage
```

このコマンドを実行するとテストが実行されて、coverageフォルダに結果が格納されます。
coverage/lcov-report/index.htmlにはhtml形式でレポートが表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/368ea8fd-33f7-abb9-de2a-0177d97af470.png)

## Jenkins用のテストレポートを作成
Jenkins用のファイルを出力するには下記のプラグインが必要になります。
https://www.npmjs.com/package/jest-jenkins-reporter

以下のコマンドでインストールしてください。

```
npm install --save-dev jest-jenkins-reporter
```

レポートの出力方法の指定はjest.config.jsとpackage.jsonを使用します。

```js:jest.config.js
// 略
 testResultsProcessor: "jest-jenkins-reporter",
// 略
```

```json:package.json
   "jestSonar": {
     "reportPath": "reports",
     "reportFile": "test-reporter.xml",
     "indent": 4
   }
```

この設定を行った後にjestを実行するとreportsフォルダにtest-reporter.xmlに保存されます。
Jenkinsはこれを利用することで単体テストの結果を取得します。

## VSCodeを使用したテストコードのデバッグ方法
VSCodeでテストコードをデバッグするにはワークスペースに以下のファイルを作成します。

```json:.vscode/launch.json
   {
     "version": "0.2.0",
     "configurations": [
       
       {
         "type": "node",
         "request": "attach",
         "name": "Attach",
         "port": 9229
       }
     ]
   }
```

次にターミナル上で以下のコマンドを実行します。

```
node --inspect-brk node_modules/.bin/jest --runInBand __tests__/matcher.test.js
```

この後に、VSCodeのAttachボタンを押下することでデバッグ実行が可能になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/dcea3f39-18fc-a00b-fe4f-cb0b438fc6c9.png)

以下は実際の画面操作のスクリーンキャプチャとなります。
![debug.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/88654ca4-bd78-55d4-1d64-fcf2c7c184bf.gif)

## VSCodeのテストランナー

### Jest Test Explore
**(2021/11追記)**
**vscode-jestを導入した方がいいです。Jest Test Explorerと同様な操作感で使用でき、vueでも安定して動作します。**
https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest

Jest Test Explorerを使用することでVSCode上でテストが実行できる。
ここのテストを選択してテストを簡単に実行することが可能。

https://marketplace.visualstudio.com/items?itemName=kavod-io.vscode-jest-test-adapter
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/29467b3d-216e-5bd5-f629-23f4addb1413.png)


このプラグインはnode_modules/.bin/jestを使用して実行している。
このvueで使用する場合は以下のようにパスを変更する必要がある

```.vscode/settings.json
{
    "jestTestExplorer.pathToJest": "/full/workspace/path/node_modules/.bin/vue-cli-service test:unit"
}
```

## 非同期の試験
https://jestjs.io/docs/ja/asynchronous

### do引数の利用
テストメソッドの引数にdoneを与えて、テストメソッド中にそのdoneを実行することで任意のタイミングでテストの終了を通知することができます。

```js:async.test.js
 function async_func() {
   return new Promise(resolve =>{
     console.log('start async_func');
     setTimeout(function() {
       console.log('end async_func');
       resolve('done!');
     }, 100);
   });
 }
 
 test('async callback test', done=> {
   async_func().then(msg => {
     expect(msg).toBe('done!');
     done();
   });
 });
```

doneを使用してテストメソッド中に呼ばれない場合はタイムアウトのエラーとなります。

```js:エラーが発生するサンプル
 test('async callback not called', done=> {
   async_func();
 }); 
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/048836cd-2171-f6b1-2a61-da42779ef1c5.png)

### Promisesの試験
テスト対象のメソッドがPromisesを返す場合、テストメソッドでそのPromisesを返すことで、それが解決するまでテストメソッドの完了を待つことができます。
マッチャー関数の[resolves](https://jestjs.io/docs/ja/expect#resolves)/[rejects](https://jestjs.io/docs/ja/expect#rejects)を使用してPromissesの結果を判定することも可能です。


```js:async.test.js
 function async_func() {
   return new Promise(resolve =>{
     console.log('start async_func');
     setTimeout(function() {
       console.log('end async_func');
       resolve('done!');
     }, 100);
   });
 }
 
 function async_func_err() {
   return new Promise((resolve, reject) =>{
     console.log('start async_func_err');
     setTimeout(function() {
       console.log('end async_func_err');
       reject('error!');
     }, 100);
   });
 }
 test('async promiss resolve test 1', ()=> {
   return async_func().then(msg => {
     console.log('resolve test');
     expect(msg).toBe('done!');
   });
 });
 
 test('async promiss resolve test 2', ()=> {
   return expect(async_func()).resolves.toBe('done!');
 });
 
 test('async promiss reject test', ()=> {
   return async_func_err().catch(msg => {
     console.log('reject test');
     expect(msg).toBe('error!');
   });
 });
 
 test('async promiss reject test 2', ()=> {
   return expect(async_func_err()).rejects.toBe('error!');
 });
```

### async/awaitの使用
async と awaitをテスト中に使用することも可能です。

```js:async.test.js
 test('await ', async ()=> {
   const ret = await async_func();
   console.log(ret);
   expect(ret).toBe('done!');
 });
 
 test('await 2', async () => {
    await expect(async_func()).resolves.toBe('done!');
 });
```

## setup,teardown使用したテスト毎の処理
各テストの開始前と開始後に特定の処理を実行することができます。

 - [beforeAll](https://jestjs.io/docs/ja/api#beforeallfn-timeout)
    - ファイル内のいずれかのファイルを実行する場合に実行される。
 - [afterAll](https://jestjs.io/docs/ja/api#afterallfn-timeout)
    - ファイル内の全てのテストが完了した際に実行される。
 - [beforeEach](https://jestjs.io/docs/ja/api#beforeallfn-timeout)
    - ファイルまたはテストスイート内の各テストが実行される前に、関数を実行する。
 - [afterEach](https://jestjs.io/docs/ja/api#aftereachfn-timeout)
    - ファイルまたはテストスイート内の各テストが完了した際に、関数を実行する。

https://jestjs.io/docs/ja/setup-teardown

これらの関数はpromiseを返すことができ、jestはそのpromiseが解決するまでは処理が進みません。このデフォルトのタイムアウトは5000msとなっており、以下のように第二引数でタイムアウトの時間を調整することが可能です。

```js
// 500msタイムアウトするのでエラーとなるサンプル
afterAll(()=> {
  console.log('afterall');
  return new Promise(r=> setTimeout(r, 1000));
}, 500);
```

beforeEach/afterEachは階層化されたdescribeごとに設定することができます。下記のコードはその実行順を検証したものとなります。

```js:setup_teardown.test.js
 beforeAll(() => {
   console.log('beforeAll'); 
 });
 
 afterAll(() => {
   console.log('afterAll'); 
 });
 
 beforeEach(() => {
   console.log('before Each アウトスコープ');
 });
 
 afterEach(() => {
   console.log('after Each アウトスコープ');
 });
 
 test('test1', ()=> {
   console.log('tes1 を実行する');
 });
 
 test('test2', ()=> {
   console.log('tes2 を実行する');
 });
 
 describe('テストスィート', ()=> {
   beforeEach(() => {
     console.log('before Each スコープ内');
   });
   afterEach(() => {
     console.log('after Each スコープ内');
   });
   test('test3', ()=> {
     console.log('test3 を実行する');
   });
   test('test4', ()=> {
     console.log('test4 を実行する');
   });
 });
 
 test('test5', ()=>{
   console.log('test5を実行する');
 });
```

このコードの実行結果は以下のようになります。
describe中で指定したbeforeEach,afterEachは、その構造を考慮した順番で実行されていることが確認できます。

```実行結果
  ● Console
 
     console.log
       beforeAll
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:2:11)
 
     console.log
       before Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)
 
     console.log
       tes1 を実行する
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:18:11)
 
     console.log
       after Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)
 
     console.log
       before Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)
 
     console.log
       tes2 を実行する
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:22:11)
 
     console.log
       after Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)
 
     console.log
       before Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)
 
     console.log
       before Each スコープ内
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:27:13)
 
     console.log
       test3 を実行する
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:33:13)
 
     console.log
       after Each スコープ内
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:30:13)
 
     console.log
       after Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)
 
     console.log
       before Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)
 
     console.log
       before Each スコープ内
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:27:13)
 
     console.log
       test4 を実行する
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:36:13)
 
     console.log
       after Each スコープ内
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:30:13)
 
     console.log
       after Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)
 
     console.log
       before Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)
 
     console.log
       test5を実行する
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:41:11)
 
     console.log
       after Each アウトスコープ
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)
 
     console.log
       afterAll
 
       at Object.<anonymous> (__tests__/setup_teardown.test.js:6:11)

```

## 特定のテストを実行するまたは実行しない。
test,it,describeには[skip](https://jestjs.io/docs/ja/api#testskipname-fn)、[only](https://jestjs.io/docs/ja/api#testonlyname-fn-timeout)を使用してテストを実行するかどうかの制御を行うことができます。

skipは動かなくなったテストを暫定的にスキップする場合や、負荷が高くて通常は実行したくないテストを弾く場合に使用します。
まだ未実装のことを現したい場合は[todo](https://jestjs.io/docs/ja/api#testtodoname)を使用しましょう。todoではエラーを出力します。

onlyはデバッグ中に特定のテストのみを動作させる場合に使用します

なお、MsTestにあるpriorityは存在しないので、優先度を指定してのテストは行えないようです。

```js:スキップの例.js
 describe('desc1', () => {
   test.skip('test1', () => {
   });
 
   test('test2', () => {
     // これだけ実行される
   });
 });
 
 describe.skip('desc2', ()=> {
   test('test3', () => {
   });
 
   test('test4', () => {
   });
 });
```


```js:onlyの例.js
 describe.only('desc1', () => {
   test('test1', () => {
     // 実行される
   });
 
   test('test2', () => {
     // 実行される
   });
 });
 
 describe('desc2', ()=> {
   test('test3', () => {
   });
 
   test('test4', () => {
   });
 });
 
 describe.only('desc3', ()=> {
   test.only('test5', ()=> {
     // 実行される
   });
 
   test('test6', ()=> {
   });
 });
```

## モック
jestではモックが使用できます。これを利用してテスト対象に依存する関数をテストに都合のいい振る舞いにすることができます。

### 特定の関数をモックにする例
[spyOn](https://jestjs.io/docs/ja/jest-object#jestspyonobject-methodname )を用いることで特定の関数をモックにすることができます。

**テスト対象の関数**

```js:calc.js
 function sum(a, b) {
   return a + b;
 }
 function minus(a, b) {
   return a - b;
 }
 module.exports = {
   sum : sum,
   minus : minus
 }
```

**テストコード**

```js:__tests__/sample.test.js
 const calc = require('../calc');
 
 test('test sum.', ()=> {
   expect(calc.sum(1, 2)).toBe(3);
 });
 test('特定の関数についてmockを使用して特定の値を返却する', ()=> {
   // モックを設定する
   console.log('spyOnの前', calc.sum, calc.minus);
   jest.spyOn(calc, 'sum').mockReturnValue(5);
   console.log('spyOnの後', calc.sum, calc.minus);
 
   // モックを呼び出す
   expect(calc.sum(1,2)).toBe(5);
 
   // モックを元の関数に戻す
   calc.sum.mockRestore();
   console.log('mockRestoreの後', calc.sum, calc.minus);
   expect(calc.sum(1,2)).not.toBe(5);
 });
 test('特定の関数についてmockを使用して特定の関数を実行する', ()=> {
   jest.spyOn(calc, 'sum').mockImplementation((a,b) => {
     console.log('mock function');
     return 100;
   });
   expect(calc.sum(1,2)).toBe(100);
   jest.restoreAllMocks();
   expect(calc.sum(1,2)).toBe(3);
 });
```

### モジュール内の関数を全てモックにする
[jest.mock](https://jestjs.io/docs/ja/jest-object#jestmockmodulename-factory-options)を使用することで、jest.mockを実行したファイル内では、そのモジュールをモックとして使用します。
この例ではテストコード中でfsモジュールの関数をモックとして使用します。

**テスト対象のモジュール**

```js:read_file.js
 const fs = require('fs');
 
 module.exports = function(path) {
   var text = fs.readFileSync(path, 'utf8');
   var lines = text.toString().split('\n');
   return lines;
 }
```

**テストコード**

```js:テストコード.js
 const read_file = require('../read_file');
 const fs = require('fs')
 jest.mock('fs');
 test('test 1.', ()=>{
   const contents = 'test\ntest2\ntest3'
   fs.readFileSync.mockReturnValue(contents);
   const results = read_file('test.txt')
   console.log(results);
   expect(results).toEqual(['test','test2','test3']);
 });
 test('test 2.', ()=>{
   const contents = ''
   fs.readFileSync.mockReturnValue(contents);
   const results = read_file('test.txt')
   console.log(results);
   expect(results).toEqual([""]);
 });
```

### マニュアルモックの使用例
マニュアルモックはモックデータを返すスタブを別ファイルに作成することができます。
&#095;&#095;mocks&#095;&#095;サブディレクトリにモックモジュールを作成します。

https://jestjs.io/docs/ja/manual-mocks

**モックモジュール**

```js:__mocks__/fs.js
 'use strict';
 const fs = jest.createMockFromModule('fs');
 fs.readFileSync = function(path, enc) {
   return '1234\nabcde\n56789\n';
 }
 module.exports = fs;
```

**テストコード**
テストコードではfs.readFileSyncを使用するとモックモジュールで指定した関数が実行されます。

```js:__tests__/manual_mock.test.js
 const read_file = require('../read_file');
 const fs = require('fs')
 jest.mock('fs');
 test('manual mock.', ()=>{
   const results = read_file('test.txt')
   expect(results).toEqual(['1234','abcde','56789', '']);
 });
```

### タイマーと時刻の偽装
[useFakeTimers](https://jestjs.io/docs/ja/jest-object#jestusefaketimersimplementation-modern--legacy) を使用することでsetTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate、clearImmediateなどのタイマー関係の関数やシステム時刻の偽装を行えます。

#### システム時刻の偽装
[setSystemTime](https://jestjs.io/docs/ja/jest-object#jestsetsystemtimenow-number--date)で偽の時間を指定することができます。ただし、これはuseFakeTimersでmodernを指定した時のみ有効です。
時刻を偽装しているときに本当の時刻を取得したい場合は[getRealSystemTime](https://jestjs.io/docs/ja/jest-object#jestgetrealsystemtime)を使用します。
時刻の偽装を止める場合は[useRealTimers](https://jestjs.io/docs/ja/jest-object#jestuserealtimers)を使用します。

```時刻の偽装のテストコードの例.js
 test('時計のモックの確認', ()=> {
 
   const dumytime = new Date(2020, 0, 1, 23, 55,40);
   expect(new Date()).not.toEqual(dumytime);
 
   // Fakeの時間を使用する
   jest.useFakeTimers('modern');
   jest.setSystemTime(new Date(2020, 0, 1, 23,55,40));
   expect(new Date()).toStrictEqual(dumytime);
 
   console.log(new Date(jest.getRealSystemTime()));
   expect(new Date(jest.getRealSystemTime())).not.toStrictEqual(dumytime);
 
   // 通常の時間を使う
   jest.useRealTimers();
   expect(new Date()).not.toEqual(dumytime);
 });
```

#### タイマーの挙動の偽装
以下ではsetTimeoutの偽装を行いモックとして利用する例を示します。
https://jestjs.io/docs/ja/timer-mocks

**テスト対象の関数**

```js:timerGame.js
 'use strict';
 
 function timerGame(callback) {
   console.log('Ready....go!');
   setTimeout(() => {
     console.log("Time's up -- stop!");
     callback && callback();
   }, 1000);
 }
 
 module.exports = timerGame;
```

**テストコード**

```js:タイマーのモックの操作例.js
 test('waits 1 second before ending the game', () => {
   console.log(setTimeout);
 
   // setTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate
   // そして clearImmediateを偽装する
   jest.useFakeTimers();
 
   // setTimeoutがmockConstructorになる
   console.log(setTimeout);
 
   const timerGame = require('../timerGame');
   timerGame();
 
   // setTimeoutがどのように実行されたかを確認する
   expect(setTimeout).toHaveBeenCalledTimes(1);
   expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
 
   // タイマーシステムが保留しているあらゆるタイマーを削除します。
   // これを実行しないと他のテストでjest.runAllTimersを実行した際にsetTimeoutのコールバックが動作する
   jest.clearAllTimers()
 
   // 本物のTimer関数を使用するように戻す。
   jest.useRealTimers()
 });
```

useFakeTimers()を使用すると以降、setTimeoutがモックの関数になります。

##### 偽装したタイマーを進める
偽装したタイマーは[advanceTimersByTime](https://jestjs.io/docs/ja/jest-object#jestadvancetimersbytimemstorun)で指定のmsを経過したことにすることができます。

```js:タイマーを進めるテストコード.js
 test('タイマーを経過させる', () => {
   jest.useFakeTimers();
 
 
   const timerGame = require('../timerGame');
   const callback = jest.fn();
 
   timerGame(callback);
 
   // At this point in time, the callback should not have been called yet
   expect(callback).not.toBeCalled();
 
   // 半分だけ進める
   jest.advanceTimersByTime(500);
 
   expect(callback).not.toBeCalled();
 
   // 残り半分を進める
   jest.advanceTimersByTime(500);
 
   // Now our callback should have been called!
   expect(callback).toBeCalled();
   expect(callback).toHaveBeenCalledTimes(1);
 
   jest.clearAllTimers()
   jest.useRealTimers()
 });
```

##### 偽装したタイマーのコールバックを実行する
偽装されたsetTImerは[jest.runAllTimers](https://jestjs.io/docs/ja/jest-object#jestrunalltimers)または[jest.runOnlyPendingTimers](https://jestjs.io/docs/ja/jest-object#jestrunonlypendingtimers)で設定したコールバックを実行することができます。
runAllTimersでは全てのタイマーを実行し、runOnlyPendingTimersは保留中のタイマーのみ実行します。
再帰的なタイマーの場合、runAllTimersを使用すると無限ループになるので、runOnlyPendingTimersを使用するようにします。

**テスト対象のコード**
再帰的なタイマーを使用しているとします。

```js:infiniteTimerGame.js
 // infiniteTimerGame.js
 'use strict';
 
 function infiniteTimerGame(callback) {
   console.log('Ready....go!');
 
   setTimeout(() => {
     console.log("Time's up! 10 seconds before the next game starts...");
     callback && callback();
 
     // Schedule the next game in 10 seconds
     setTimeout(() => {
       infiniteTimerGame(callback);
     }, 10000);
   }, 1000);
 }
 
 module.exports = infiniteTimerGame;
```

**テストコード**

```js:テストコードの例.js
 test('再帰的なタイマーを実行した場合', () => {
   jest.useFakeTimers();
 
 
   const infiniteTimerGame = require('../infiniteTimerGame');
   const callback = jest.fn();
 
   infiniteTimerGame(callback);
 
 
   // At this point in time, the callback should not have been called yet
   expect(callback).not.toBeCalled();
 
 
   // これだと無限ループになる
   // jest.runAllTimers();
   jest.runOnlyPendingTimers();
 
   // Now our callback should have been called!
   expect(callback).toBeCalled();
   expect(callback).toHaveBeenCalledTimes(1);
 
   expect(setTimeout).toHaveBeenCalledTimes(2);
 
   jest.clearAllTimers()
   jest.useRealTimers()
 });
```

## 期待値の確認
[expect](https://jestjs.io/docs/ja/expect)を使用して確認します。

```js:テストコード.js
 test('test', ()=> {
   // test1()の結果が1であるか確認する
   expect(test1()).toBe(1);
   // test1()の結果が2でないことを確認する。
   expect(test1()).not.toBe(2);
 }
```

notを使用することで条件の反転が可能です。


 - [toBe](https://jestjs.io/docs/ja/expect#tobevalue)
   - プリミティブ値を比較したり、オブジェクトインスタンスの参照IDを確認したりします。
   - 浮動小数点のチェックには使用しないでください。

 - [toBeCloseTo](https://jestjs.io/docs/ja/expect#tobeclosetonumber-numdigits)
    - 浮動小数点の近似的な値を比較します。
    - デフォルトでは小数点の第二位までチェックします。

```js:浮動小数点のテスト例.js
 test('浮動小数点 - 桁数の確認', ()=> {
   // デフォルトでは有効桁二桁まで確認するーこれは合格する
   expect(0.1+0.201).toBeCloseTo(0.3);
 
   // 第二引数に有効桁を指定する。ーこれはNGとなる
   expect(0.1+0.201).toBeCloseTo(0.3,3);
 })
```

 - [toBeTruthy](https://jestjs.io/docs/ja/expect#tobetruthy)
 - [toBeFalsy](https://jestjs.io/docs/ja/expect#tobefalsy)
    - 真偽値のチェックを行う。
 
 - [toBeUndefined](https://jestjs.io/docs/ja/expect#tobeundefined)
    - undefinedか否かの確認
 - [toBeDefined](https://jestjs.io/docs/ja/expect#tobedefined)
    - undefinedではないことの確認

 - [toEqual](https://jestjs.io/docs/ja/expect#toequalvalue)
    - オブジェクトが再起的に一致するか確認する。

```js:toEqualのテストコード.js
 function test5() {
   return {
     name : 'Abc',
     age : 15,
     group: {
       name : "Def"
     }
   }
 }
 
 test('equal', ()=> {
   expect(test5()).toEqual({
     name : 'Abc',
     age : 15,
     group: {
       name : "Def"
     }
   });
 }); 
```

 - [toStrictEqual](https://jestjs.io/docs/ja/expect#tostrictequalvalue)
    - toEqualに加えて型が一致するかも確認している

```js:toStrictEqualのサンプルコード
 class LaCroix {
   constructor(flavor) {
     this.flavor = flavor;
   }
 }
 test('equal-strict',()=> {
   expect(new LaCroix('レモン')).toEqual({flavor: 'レモン'});
   expect(new LaCroix('レモン')).not.toStrictEqual({flavor: 'レモン'});
 });
```

 - [toContain](https://jestjs.io/docs/ja/expect#tocontainitem)
    - 配列内に特定の値が含まれているか確認する
    - 文字列内に特定の文字列が含まれているか確認する

```js:テストコード.js
 test('contein1', ()=> {
   const lst = ['abc', 'efg', 'hij'];
   expect(lst).toContain('efg');
 
   expect('abcdefghij').toContain('efg');
 });
```

 - [toContainEqual](https://jestjs.io/docs/ja/expect#tocontainequalitem)
    - オブジェクトの配列中に同じ構造で同じ値のオブジェクトが含まれているか確認する

```js:サンプルコード
 test('contein2', ()=> {
   const list = [
     {a:1234},
     {b:2345, c:'def'},
     {c:1111, d: { e:2222}}
   ];
   expect(list).toContainEqual({a:1234});
   expect(list).toContainEqual({b:2345, c:'def'});
   expect(list).toContainEqual({c:1111, d:{e:2222}});
 });
```

 - [toThrow](https://jestjs.io/docs/ja/expect#tothrowerror)
   - 例外が発生することを確認する
   - 引数としてエラメッセージの文字、正規表現、エラーオブジェクト、クラスを与えることができる

```js:サンプルコード.js
 test.only('throw', ()=> {
   expect(()=> {
     error_func();
   }).toThrow();
 
   expect(()=> {
     error_func();
   }).toThrow('Error');
 
   expect(()=> {
     error_func();
   }).toThrow(/rr/);
 
   expect(()=> {
     error_func();
   }).toThrow(new Error('Error'));
 
   expect(()=> {
     error_func();
   }).toThrow(Error);
 
 });
```

## テストの同時実行
[test.concurrent](https://jestjs.io/docs/ja/api#testconcurrentname-fn-timeout)を使用することでテストを同時に実行することが可能です。
ただし26.4時点で実験的な機能なため以下のような問題が存在しています。
https://github.com/facebook/jest/labels/Area%3A%20Concurrent

以下のコードはconcurrentを使用しないケースと使用したケースの挙動を確認しています。

```js:concurrentを使用しないケース
test('addition of 2 numbers', () => {
  console.log('test1');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test1');
      expect(5 + 3).toBe(8);
      resolve('done!');
    }, 3000);
  });
});

test('subtraction 2 numbers', () => {
  console.log('test2');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test2');
      expect(5 - 3).toBe(2);
      resolve('done!');
    }, 1000);
  });
});
```

この実行結果は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/8b8a65a6-669d-d918-5b9e-4a7a5e616c4d.png)
１つ目のテストケースが完了した後に、２つ目のテストケースが完了することがわかります。

以下はconcurrentを使用したケースです。

```js:concurrentを使用した場合
test.concurrent('addition of 2 numbers', () => {
  console.log('test1');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test1');
      expect(5 + 3).toBe(8);
      resolve('done!');
    }, 3000);
  });
});

test.concurrent('subtraction 2 numbers', () => {
  console.log('test2');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test2');
      expect(5 - 3).toBe(2);
      resolve('done!');
    }, 1000);
  });
});
```

この実行結果は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/dd4999c8-f99c-2c7c-e674-4738f49a6736.png)

この結果より同時にテストケースが動作していることが確認できます。


## VueでJestを利用する

前提：
@vue/cli 4.5.6　がインストールされている。

### 単純な作成例
「vue create」を実行した際にマニュアルで設定を行うと単体テストをjestで行えるように指定できます。
ここでは後からjestを導入することを想定した手順とします。

以下のコマンドを実行することで、vueプロジェクトにjestを使うためのプラグインをインストールしています。

```text:コマンド
 # デフォルトでvueのプロジェクトを作成する
 vue create vue_test
 cd vue_test
 # テストツールのインストール
 npm install --save-dev @vue/cli-plugin-unit-jest vue-template-compiler @vue/test-utils
 mkdir -p tests/unit
```

jest用の設定ファイルを作成します。

```js:jest.config.js
 module.exports = {
   preset: '@vue/cli-plugin-unit-jest'
 }
```

tests/unitフォルダにテストコードを追加します。

```js:tests/unit/example.spec.js
 import { shallowMount } from '@vue/test-utils'
 import HelloWorld from '@/components/HelloWorld.vue'
 
 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
     expect(wrapper.text()).toMatch(msg)
   })
 })
```

[cli-plugin-unit-jest](https://cli.vuejs.org/core-plugins/unit-jest.html#transform-dependencies-from-node-modules)プラグインをインストールした後は、[vue-cli-service](https://cli.vuejs.org/guide/cli-service.html#using-the-binary)コマンドを使用することで単体テストが行えるようになります。

```text:テスト実行用コマンド
npx vue-cli-service test:unit
```

### スナップショットテスト
jestはスナップショットテストをサポートしており、レンダリングしたHTMLが前回のテスト時と変更されたかをチェックすることができます。

https://jestjs.io/docs/ja/snapshot-testing#inline-snapshots
https://system.blog.uuum.jp/entry/2017/12/12/110000

スナップショットテストをjest+vueで行うには[vue-server-renderer](https://github.com/vuejs/vue/tree/dev/packages/vue-server-renderer)をインストールして、node.jsでVueをレンダリングできるようにします。

```text:インストール用のコマンド
npm install --save-dev vue-server-renderer
```

**テストコード**

```js:test/unit/example.spec.js
 import { shallowMount } from '@vue/test-utils'
 import HelloWorld from '@/components/HelloWorld.vue'
 import { createRenderer } from 'vue-server-renderer'
 
 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
     expect(wrapper.text()).toMatch(msg)
   })
   it('snap shot test', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
 
     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer()
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err)
       // 最新のスナップショットと一致するか比較
       expect(str).toMatchSnapshot()
     })    
   })
 })
```

このテストコードを実行すると、tests/unitに&#095;&#095;snapshots&#095;&#095;フォルダが作成されます。
何も変更せずにテストを際実行するとテストは合格します。
そこで、テストコードの以下を修正します。

```js:
// 略
const msg = 'new message!!'
// 略
```

修正後にテストを際実行すると以下のようなエラーが発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/7d29441e-a36b-4768-14d1-19e7709a7bb4.png)

もし、このエラーが予定通りの場合はスナップショットを更新してテストを合格にする必要があります。そのためには以下のコマンドを実行します。

```text:スナップショットの更新のコマンド
npx vue-cli-service test:unit -u
```

このコマンドを実行後、スナップショットは更新されて、テストが合格するようになります。
この例のように[toMatchSnapshot](https://jestjs.io/docs/ja/expect#tomatchsnapshotpropertymatchers-hint)を使用すると外部ファイルにスナップショットを保存しますが、[toMatchInlineSnapshot](https://jestjs.io/docs/ja/expect#tomatchinlinesnapshotpropertymatchers-inlinesnapshot)を使用することでテストコード中に期待するHTMLを記載することができます。

以下はインラインスナップショットのテストコードのサンプルです。初回実行前には期待値は存在しません。

```javascript
   it("inline snap shot test"", () => {
     const msg = "new message!!";
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     });
 
     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer();
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err);
       expect(str).toMatchInlineSnapshot();
     });
   });
```

テストを実行するとテストコードは次のように変更されます。

```javascript

   it("inline snap shot test", () => {
     const msg = "new message!!";
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     });
 
     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer();
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err);
       // 最新のスナップショットと一致するか比較
       expect(str).toMatchInlineSnapshot(`
         <div class="hello">
           <h1>new message!!</h1>
           <p>
             For a guide and recipes on how to configure / customize this project,<br>
             check out the
             <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
           </p>
           <h3>Installed CLI Plugins</h3>
           <ul>
             <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
             <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
           </ul>
           <h3>Essential Links</h3>
           <ul>
             <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
             <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
             <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
             <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
             <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
           </ul>
           <h3>Ecosystem</h3>
           <ul>
             <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
             <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
             <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
             <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
             <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
           </ul>
         </div>
       `);
     });
```

# トラブルシュート
## requireのキャッシュをクリアしたい
jest使用中にrequireのキャッシュをクリアしたいケースがある。
require.cacheの内容を削除することで対応できるが、jest使用中は、ここにキャッシュが格納されない。
beforeEachにて[resetModules](https://jestjs.io/docs/jest-object#jestresetmodules)を実行することで、読み込んでいたrequireのキャッシュを消すことが可能。

```js
beforeEach(() => {
  jest.resetModules()
})

```

https://stackoverflow.com/questions/48042200/invalidate-node-cache-when-using-jest

## 依存しているファイルの一部だけモックしたい

```js
/* eslint-disable import/first */
jest.mock('src/utility', () => ({
  ...(jest.requireActual('src/utility')), // 特定の関数以外は本物を使う
  sleep: jest.fn()
}))
import { sleep } from 'src/utility' // モックされた関数

import hoge from './src/hoge.js' // sleepに依存しているモジュールでもsleepはモックされた関数となる
```

https://stackoverflow.com/questions/39755439/how-to-mock-imported-named-function-in-jest-when-module-is-unmocked
https://jestjs.io/docs/bypassing-module-mocks

## いつも動いてるテストが突然不安定になった
jestは規定の動きではキャッシュを保存しています。
このキャッシュを以下のコマンドでクリアします。

```
npx jest --clearCache 
```

https://jestjs.io/ja/docs/cli#--clearcache

## Canvasを使用するテストをしたい
jestの仮想DOMはCanvasをサポートしていないので、getContextがnullになります。
getContextをモックアップする方法もありますが、面倒ならば
jest-canvas-mockを使用しましょう。

https://github.com/hustcc/jest-canvas-mock