VSCodeでイマドキのJavaScriptの開発環境をサクッと作ろうぜ(自動テスト編)

# はじめに
イマドキのJavaScript開発環境記事第2弾。

[第1弾はこちら](https://qiita.com/neruneruo/items/c940af6c85519fd63648)。

今回は、自動テストの設定を作っていく。

# Jestの自動テスト
JavaScriptの自動テストは有名どころがいくつかあるが、最近はJestが良い感じっぽいので、これを使うための設定を行っていく。

## 全体のファイル構成
今回は少し複雑なので、ファイル構成から俯瞰しよう。
テスト用コードは、Jestの推奨である__test___配下に配置する。メインのコードはcontents配下だ。

```
<プロジェクトルート>
├── .jestrc.js
├── __tests__
│   ├── __snapshots__
│   │   └── employeeMain.spec.js.snap
│   ├── employeeMain.spec.js
│   └── mock
│       └── mockAxios.js
├── contents
│   ├── app.js
│   ├── employeeMain.vue
│   └── mixin.js
└── package.json
```

## Jest関連モジュールのインストール
まずは前回同様、以下をpackage.jsonに追記して、```npm install```しよう。
例によって、バージョン指定が面倒なので、今動いているバージョンを書いておく。

```json:package.json(抜粋)
  "devDependencies": {
    "axios-mock-adapter": "^1.20.0",
    "babel-jest": "^26.6.3",
    "flush-promises": "^1.0.2",
    "jest": "^26.6.3",
    "jest-serializer-vue": "^2.0.2",
    "vue-jest": "^4.0.1",
    "vue-loader": "^15.9.8",
    "vue-server-renderer": "^2.6.14",
  }
```

## Jest設定ファイルの配置
Jestの設定ファイルはデフォルトはjest.config.jsというファイル名だが、この手の設定ファイルはツールによってバラバラで分かりにくいので、今回の一連の記事では、ドット始まりで統一する。

```javascript:.jestrc.js
module.exports = {
  verbose: true,
  transform: {
    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
    '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest',
  },
  // テスト対象の拡張子を列挙する
  moduleFileExtensions: [
    'js',
    'vue',
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
  },
  testPathIgnorePatterns: [
    '/mock/',
    '/__snapshots__/'
  ],
  // スナップショットの設定
  // カスタムシリアライザを使用して読みやすいスナップショットを表示する
  // 改善されたスナップショットをJestのスナップとして渡す
  snapshotSerializers: [
    '<rootDir>/node_modules/jest-serializer-vue'
  ],
  // カバレッジの設定
  collectCoverage: true,
  collectCoverageFrom: [
    '**/*.{js,vue}',
    // 設定ファイルを除外
    '!**/.*',
    // 外部モジュールを除外
    '!**/node_modules/**',
    '!**/mock/**'
  ],
  coverageReporters: [
    'text'
  ]
}
```

なお、標準と異なる設定ファイルを用いる場合は、```--config```オプションでファイルを指定する。package.jsonに以下の設定を追加しよう。

```diff_json:package.json(抜粋)
  "scripts": {
+   "test": "jest --config ./.jestrc.js",
  }
```

## axios-mock-adapterでAxiosをモックする
さて、Vue.jsでSPAを作ろうとしている以上、このスクリプト単独で何かをするというよりも、サーバAPIと連動することの方が多いだろう。
Jest+Axiosでは、Jestの設定の```testEnvironment```がデフォルトの```jsdom```だとネットワークエラーが出てしまいうまく動作しない。
一方で、これを```node```に変更してしまうと、Vue.jsが```jsdom```モードでないと動作しないため、うまく動かなくなってしまう。

そのための設定が以下である。

```diff_json:package.json(抜粋)
  "devDependencies": {
+   "axios-mock-adapter": "^1.20.0",
    "babel-jest": "^26.6.3",
    "flush-promises": "^1.0.2",
    "jest": "^26.6.3",
    "jest-serializer-vue": "^2.0.2",
    "vue-jest": "^4.0.1",
    "vue-loader": "^15.9.8",
    "vue-server-renderer": "^2.6.14",
  }
```

モック化する情報を以下のように設定する。
```return```する第2要素は、何でもよいのでモックで応答したい内容を記載する。

```javascript:mock/mockAxios.js
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'

const mockAxios = new MockAdapter(axios)

mockAxios.onGet(process.env.APIGATEWAY_INVOKE_URL + '/employee').reply(config => {
  if (config.params.id === '00001') {
    return [
      200,
      [
        { department: '001002', id: '00001', age: '36', dept_id: '001', dept_name: '開発部', sect_id: '002', sect_name: '第二開発課' },
        { department: '002001', id: '00001', age: '36', dept_id: '002', dept_name: '総務部', sect_id: '001', sect_name: '総務課' },
        { department: '007001', id: '00001', age: '36', dept_id: '007', dept_name: '製造部', sect_id: '001', sect_name: '第一製造課' }
      ]
    ]
  }
})
```

あとは、これをテスト対象のspec.jsファイルから、```import '@/__tests__/mock/mockAxios.js'```とでもして呼び出せば良い。

## モックしたAxiosがうまく同期できない場合、flush-promisesを使う
Vue.jsの中でasync～awaitしても、Jest側でうまく同期できないことがある。
その場合は、flush-promisesのモジュールを使おう。

```diff_json:package.json(抜粋)
  "devDependencies": {
    "axios-mock-adapter": "^1.20.0",
    "babel-jest": "^26.6.3",
+   "flush-promises": "^1.0.2",
    "jest": "^26.6.3",
    "jest-serializer-vue": "^2.0.2",
    "vue-jest": "^4.0.1",
    "vue-loader": "^15.9.8",
    "vue-server-renderer": "^2.6.14",
  }
```

上記のモジュールをインストールした後に、

```javascript:test.spec.js(抜粋)
import flushPromises from 'flush-promises'

// (中略)

  // 待ち合わせしたい箇所
  await flushPromises()
```

とすることで、一旦非同期で動作しているところをすべて待ち合せることができる。

## jest-serializer-vueでスナップショットテストを行う
スナップショットテストとは、前回のレンダリングしたHTMLを保存しておき、それと今回のテスト結果を比較することでデグレードしていないか確認できるテストだ。
Jest+Vue.jsでは以下のモジュールを使用する。

```diff_json:package.json(抜粋)
  "devDependencies": {
    "axios-mock-adapter": "^1.20.0",
    "babel-jest": "^26.6.3",
    "flush-promises": "^1.0.2",
    "jest": "^26.6.3",
+   "jest-serializer-vue": "^2.0.2",
    "vue-jest": "^4.0.1",
    "vue-loader": "^15.9.8",
+   "vue-server-renderer": "^2.6.14",
  }
```

設定ファイルで以下の部分がスナップショットテストの設定に該当する。

```diff_javascript:.jestrc.js(抜粋)
module.exports = {
// (中略)
+ // スナップショットの設定
+ // カスタムシリアライザを使用して読みやすいスナップショットを表示する
+ // 改善されたスナップショットをJestのスナップとして渡す
+ snapshotSerializers: [
+   '<rootDir>/node_modules/jest-serializer-vue'
+ ],
// (中略)
}
```

テストコードでは、任意の場所でスナップショットを取ることが可能だ。
それぞれのテストの最後にスナップショットを取るのが無難ではないかと考えられる。

```diff_javascript:test.spec.js(抜粋)
// テスト対象のモジュール
import employeeMain from '@/contents/employeeMain.vue'
+ // スナップショットテスト用
+ import { createRenderer } from 'vue-server-renderer'

// テストスイート
describe('employeeMain', () => {
//(中略)
+ afterEach(() => {
+   const renderer = createRenderer()
+   renderer.renderToString(wrapper.vm, (err, str) => {
+     if (err) throw new Error(err)
+     // 最新のスナップショットと一致するか比較
+     expect(str).toMatchSnapshot()
+   })
+ })
//(中略)
}
```

なお、スナップショットテストで、コード修正によって差分が出るようになるのが正しいケースもある。
このため、スナップショットを更新したい場合は、```-u```オプションで更新を行う。
以下のようにpackage.jsonに追記をして、更新可能にしておこう。
なお、CI/CDで用いるためには、この結果の__snapshot__もリポジトリに登録するようにしておこう。

```diff_json:package.json(抜粋)
  "scripts": {
    "test": "jest --config ./.jestrc.js",
+   "testUpdate": "jest --config ./.jestrc.js -u",
  }
```

## カバレッジの設定
カバレッジは、標準のJestの機能として提供されているため、追加のモジュールは必要ない。
以下の設定を入れておこう。

設定ファイルで以下の部分がスナップショットテストの設定に該当する。

```diff_javascript:.jestrc.js(抜粋)
module.exports = {
// (中略)
+ // カバレッジの設定
+ collectCoverage: true,
+ collectCoverageFrom: [
+    '**/*.{js,vue}',
+    // 設定ファイルを除外
+    '!**/.*',
+    // 外部モジュールを除外
+    '!**/node_modules/**',
+    '!**/mock/**'
+ ],
+ coverageReporters: [
+   'text'
+ ],
// (中略)
}
```

これを設定することで、テスト結果で以下のようなカバレッジレポートを出力可能だ。

```
----------------------|---------|----------|---------|---------|-------------------
File                  | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------------------|---------|----------|---------|---------|-------------------
All files             |   46.29 |     37.5 |      40 |   46.29 |                   
 app.js               |       0 |      100 |     100 |       0 | 4                 
 employeeMain.vue     |   52.38 |        0 |      60 |   52.38 | 44-50,73-80       
 mixin.js             |     100 |      100 |     100 |     100 |                   
----------------------|---------|----------|---------|---------|-------------------
```

今回作ったモックや設定ファイルがレポートに巻き込まれないよう、```collectCoverageFrom```でしっかり指定をしよう。

さて、これで、```npm run test```で自動テストが走らせられるようになった！
次回はいよいよWebpackでバンドルしてデプロイを行おう！