24
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2020-05-10

フロントエンドのテストなんて、画面開いて動かせばいいやん

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

この記事はフロントエンドのテストツールであるjestについて導入・テストコードサンプルを紹介しようと思います

  • 後々storybook編を執筆予定・・・

そもそもテストの目的

1.設計通りに機能が実装されているかの確認

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

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

  • デグレがないかの確認

3. モジュールとして適切な独立性を確保できているかの確認

  • つまりモジュールとして分離されていて、独立性が高く扱いやすいことを確認する

以上の目的をフロントエンドのテストでどうやって担保するか

  • 1についてはTypeScriptを選択することで半分以上解決できます(バグで1番多いのがNullアクセスエラーで、TypeScriptではType Guard等でNull安全性が確保される)
  • 残りの半分はstorybookによるスナップショットテスト・

この記事のゴール

  • npm run test or yarn 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 or npm run test
スクリーンショット 2020-05-10 22.55.20.jpg

Storeのテストで力尽きた・・・

  • 残りのコンポーネントテスト・E2Eテストは随時追記します。。。

フロントエンドでどこまでテストするかっていうのはチームでちゃんと決めないと不要なテストまで書いてしまいそう・・・

余談:axios-mock-adapterのレスポンスはjsonではなくて、httpレスポンスの形式で返ってくるのでちょっと不便・・・

  • コミッターになるチャンスか・・・

参考

24
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?