フロントエンドのテストなんて、画面開いて動かせばいいやん
- それはそれでアリかと思います🙂。(実際画面開けばわかるので)
- ただ、昨今Webアプリケーションにおいて求められているフロントエンド(UI/UX)の要求は高く、それに伴いソースコードも増えてきています。
- また、スピード感を持って改修・リリースするためには改修のたびに打鍵をすることは現実的ではありません。
- そこで、自動化された
Storeのテスト
・コンポーネントテスト
・E2Eテスト
をCI時に実行するようにしたいという要求が発生します。
この記事はフロントエンドのテストツールであるjest
について導入・テストコードサンプルを紹介しようと思います
- 後々storybook編を執筆予定・・・
そもそもテストの目的
1.設計通りに機能が実装されているかの確認
- この1が皆さんが使っている
テスト
の意味に近いかと思います - 全ての分岐を通ることが必要です。
2. 既存の機能が壊れていないかの確認
- デグレがないかの確認
3. モジュールとして適切な独立性を確保できているかの確認
- つまりモジュールとして分離されていて、独立性が高く扱いやすいことを確認する
以上の目的をフロントエンドのテストでどうやって担保するか
- 1についてはTypeScriptを選択することで半分以上解決できます(バグで1番多いのがNullアクセスエラーで、TypeScriptではType Guard等でNull安全性が確保される)
- 残りの半分はstorybookによるスナップショットテスト・
この記事のゴール
-
npm run test
oryarn run test
で用意したテストが全て走って結果がOKとなること
環境
- Nuxt.js:2.10.0
- TypeScript
- jest
- ※reactの場合もjestなのでタグ付けしておきました
追加するライブラリ
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作成
.babelrc
{
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
}
tsconfig.json編集
-
"@types/jest", "@types/webpack-env"
を追加しています
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にしてます。
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にテスト実行用のコマンドを追加します
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
を使用します
テストコード
- 以上をふまえてテストコードに落とし込むとこんな感じです(要所でコメント入れてます)
- 今回はAPIがまだないことを想定しており、mockの返り値がstoreに正しく格納されていることを確認しています。
- ユニットテストとして
store/モジュール
単位でinitStoreに格納。 - 内容はSESの案件を扱う業務アプリをイメージ。。。
- 1つ目のitでは案件の一覧を取得して、1つの目の案件idが1であるかを確認してます。2つ目は詳細を取得でidが1。
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にテスト用のスクリプトを追加
package.json
"scripts": {
"test": "jest --config jest.config.js"
}
-
yarn test
ornpm run test

Storeのテストで力尽きた・・・
- 残りのコンポーネントテスト・E2Eテストは随時追記します。。。
フロントエンドでどこまでテストするかっていうのはチームでちゃんと決めないと不要なテストまで書いてしまいそう・・・
余談:axios-mock-adapterのレスポンスはjsonではなくて、httpレスポンスの形式で返ってくるのでちょっと不便・・・
- コミッターになるチャンスか・・・