フロントエンドのテスト・・・やってる？？jest編

# フロントエンドのテストなんて、画面開いて動かせばいいやん
- それはそれでアリかと思います🙂。（実際画面開けばわかるので）
- ただ、昨今Webアプリケーションにおいて求められているフロントエンド（UI/UX）の要求は高く、それに伴いソースコードも増えてきています。
- また、スピード感を持って改修・リリースするためには改修のたびに打鍵をすることは現実的ではありません。
- そこで、自動化された`Storeのテスト`・`コンポーネントテスト`・`E2Eテスト`をCI時に実行するようにしたいという要求が発生します。

### この記事はフロントエンドのテストツールである`jest`について導入・テストコードサンプルを紹介しようと思います
- 後々storybook編を執筆予定・・・

# そもそもテストの目的
### 1.設計通りに機能が実装されているかの確認

- この1が皆さんが使っている`テスト`の意味に近いかと思います
- 全ての分岐を通ることが必要です。

### 2. 既存の機能が壊れていないかの確認

- デグレがないかの確認


### 3. モジュールとして適切な独立性を確保できているかの確認
- つまりモジュールとして分離されていて、独立性が高く扱いやすいことを確認する

# 以上の目的をフロントエンドのテストでどうやって担保するか
- 1についてはTypeScriptを選択することで半分以上解決できます（バグで１番多いのがNullアクセスエラーで、TypeScriptではType Guard等でNull安全性が確保される）
- 残りの半分はstorybookによるスナップショットテスト・


# この記事のゴール
- `npm run test` or `yarn run test`で用意したテストが全て走って結果がOKとなること

# 環境
- Nuxt.js：2.10.0
- TypeScript
- jest
- ※reactの場合もjestなのでタグ付けしておきました

# 追加するライブラリ
```json:package.json
 "@types/lodash": "^4.14.150",
 "@types/jest": "24.0.15"
 "@types/webpack-env": "^1.15.2",
 "@vue/test-utils": "1.0.0-beta.29",
 "axios-mock-adapter": "^1.18.1",
 "babel-core": "^7.0.0-bridge.0",
 "babel-jest": "^26.0.1",
 "eslint-plugin-jest": "^23.9.0",
 "jest": "24.8.0",
 "ts-jest": "24.0.2",
 "vue-jest": "3.0.4"

```

- Jestはそのままでは.vueファイルのテストができず、ES6の構文が使えないらしく以下でそれらを解消しています

### .babelrc作成

```json:.babelrc
{
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
    }
  }
}

```

### tsconfig.json編集

- `"@types/jest", "@types/webpack-env"`を追加しています

```json:tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "skipLibCheck": true,
    "lib": ["esnext", "esnext.asynciterable", "dom"],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": false,
    "noEmit": true,
    "noImplicitAny": false,
    "baseUrl": "",
    "paths": {
      "~/*": ["./src/*"]
    },
    "types": ["@types/node", "@nuxt/types", "@types/jest", "@types/webpack-env"]
  },
  "exclude": ["node_modules"]
}


```

### jestの設定ファイル
- 私の場合`ルート/src`として開発をしているためmoduleNameMapperにそのように記述しています
- 今回はカバレッジはfalseにしてます。

```typescript:jest.config.ts
module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/src/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  moduleFileExtensions: ['js', 'ts', 'vue', 'json'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '.*\\.(vue)$': 'vue-jest'
  },
  collectCoverage: false,
  collectCoverageFrom: ['src/**/*.vue', 'src/**/*.ts']
}

```

### package.jsonにテスト実行用のコマンドを追加します

```json:package.json
"jest": "jest --config jest.config.ts"
```

# storeのテスト
### 確認すべきは以下の点
- action：actionsを実行して想定通りにstateが更新されていること
- getter：stateから想定通りの値が取得できること

### mutationとstateは個別ではテストしない
- ※mutationはactionの結果のstateを見れば検証ができるため省略
- ※stateはactionの中でやります

### axiosをモック化
- actionsではaxiosによるAPIとの通信が行われるので、その振る舞いをモック化します
- `axios-mock-adapter`を使用します

# テストコード
- 以上をふまえてテストコードに落とし込むとこんな感じです（<font color="Salmon">要所でコメント入れてます</font>）
- 今回はAPIがまだないことを想定しており、mockの返り値がstoreに正しく格納されていることを確認しています。
- ユニットテストとして`store/モジュール`単位でinitStoreに格納。
- 内容はSESの案件を扱う業務アプリをイメージ。。。
- 1つ目のitでは案件の一覧を取得して、1つの目の案件idが１であるかを確認してます。2つ目は詳細を取得でidが１。

```typescript:hoge.spec.ts
import MockAdapter from 'axios-mock-adapter'
import axios from 'axios'
import Vuex from 'vuex'
import { createLocalVue } from '@vue/test-utils'
import { cloneDeep } from 'lodash'
import state from '~/store/modules/project/index'
import mutations from '~/store/modules/project/mutations'
import actions from '~/store/modules/project/actions'

// beforeEachで毎回Storeを生成するためにstoreを定義
const initStore = () => {
  return cloneDeep({
    state,
    mutations,
    actions
  })
}

describe('Project Modules Test', () => {
  let store
  let localValue

  beforeEach(() => {
    // eslint-disable-next-line
    localValue = createLocalVue()
    localValue.use(Vuex)
    store = new Vuex.Store(initStore())
    store.$axios = axios // @nuxtjs/axiosの代わりにaxiosを注入
  })

  const mock = new MockAdapter(axios)
  // テスト事にモックの内容をリセットするように登録する。
  // こうしないとモック内のレスポンスデータがク リアされないまま次のテストに引き継がれてしまう
  afterEach(() => {
    mock.reset()
  })

  it('get project：ID of the first data must be 1', async () => {
    const PROJECT_LIST = require('~/api/mocks/get-item.json')
    // mockを用意
    mock.onGet('/projects/list').reply(200, PROJECT_LIST['projects/list'])
    await store.dispatch('getProjectsData', 0)
    expect(store.state.projectData.data[0].id).toBe(1)
  })

  it('get projectDetail: ID must be 1', async () => {
    const PROJECT_DETAIL = require('~/api/mocks/get-detail.json')

    mock
      .onGet('/projects/detail/1')
      .reply(200, PROJECT_DETAIL['projects/detail'][0])
    await store.dispatch('getProjectsDetail', 1)
    expect(store.state.projectDetail.data[0].id).toBe(1)
  })
})


```

# テスト実行
- package.jsonのscriptsにテスト用のスクリプトを追加

```json:package.json
  "scripts": {
    "test": "jest --config jest.config.js"
  }
```
- `yarn test` or `npm run test`

<img width="1674" alt="スクリーンショット 2020-05-10 22.55.20.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303093/e1e64942-4d83-dad0-bdb4-42d1665591b6.jpeg">

### Storeのテストで力尽きた・・・
- 残りのコンポーネントテスト・E2Eテストは随時追記します。。。

# フロントエンドでどこまでテストするかっていうのはチームでちゃんと決めないと不要なテストまで書いてしまいそう・・・
### 余談：axios-mock-adapterのレスポンスはjsonではなくて、httpレスポンスの形式で返ってくるのでちょっと不便・・・
- コミッターになるチャンスか・・・

### 参考
- [jest：Using Matchers](https://jestjs.io/docs/ja/using-matchers)