7
8

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.

インターン生がVueのテストコードをJestで書いたら5000行になった話。

Last updated at Posted at 2019-01-23

ある日CTOから僕に勅命が下りました。
テストコードを作成せよ... と、

環境はVue + Jestということで色々調べてみたものの、種類が多すぎる...

https://vue-test-utils.vuejs.org/ja/guides/
https://lmiller1990.github.io/vue-testing-handbook/ja/#vue-js%E3%83%86%E3%82%B9%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%96%E3%83%83%E3%82%AF
https://jestjs.io/docs/ja/testing-frameworks

どれみればええねん。

すでにJest-test-Vueのようなライブラリが入っていたのでこれを使うことにしました。

##要件の設定
とりあえず、動くテストコードをとのお達しを受けたので、最低限成り立つものを作ろうと決めました。

Components:コンポーネントとして呼び出せるかどうかの確認
Actions: commit, dispatchが期待した回数呼び出されているかを確認。
Mutations: 処理後に期待したstateになっているかどうかを確認
Getters: stateからちゃんと取得できているかをテスト

package.json

"scripts": {
    "start": "NODE_ENV=local webpack-dev-server --hot --mode development",
    "test": "jest --maxWorkers 5",
},
"jest": {
    "moduleFileExtensions": ["js", "vue"],
    "moduleNameMapper": {
      "^@((?!(babel|vue|firebase)).*)$": "<rootDir>/src/$1"
    },
    "transformIgnorePatterns": [
      "<rootDir>/node_modules/(?!lodash-es)"
    ],
    "transform": {
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    },
 }

だいたいpackage.jsonこんな感じ。
あとVue特有の@のimport処理を解決するのが地味にしんどかったです。javascriptの正規表現勉強するはめになりました...

Circle-Ci メモリ足りなくて落ちたのでmax-worker数指定しました。

##Components
正直一番しんどかったです。
初めてのテストコードのために調べながらやっていくというものもあったんですが、何より。mockができないできない。

component.js
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import VueRouter from 'vue-router';
import show from '@pages/learnings/show.vue';
import store from '@store';

const localVue = createLocalVue();

localVue.use(Vuex);
localVue.use(VueRouter);

// TODO params could not be overwritten
describe('show.vue', () => {
  it('is callable', () => {
    try {
      const showComponent = shallowMount(show, {
        store,
        localVue,
      });
      expect(showComponent.isVueInstance()).toBeTruthy();
    } catch (err) {
      expect(err.toString().includes('params')).toBeTruthy();
    }
  });
});

普通にrouter-view, router-linkをstubする場合や、vuexをmockする場合は問題がないんです。
問題はグローバル変数である$routeをoverwriteできないこと。
調べてみるとこのような記事が出てきて、ちゃんとテストはできるんです。
しかし、storeとかと両立しようとした途端にダメになる。もうstoreインポートした時点でおじゃんですよおじゃん。両立しているコードはネットの海の中で見つけられませんでした。だれか強いひと教えてください。
仕方がないので、上記のようにtry and catchをしてエラーコードが期待された値になっているかで判定することにしました。

最低限の要件は満たせているはずです。
ここまでで2000行ぐらいかかりました。

##Actions

import actions from '@store/modules/auth/actions';
import { cloneDeep } from 'lodash-es';

let url = '';
let body = {};
const newModel = { _id: 'test_model', email: 'test_email' };

jest.mock('axios', () => ({
  get: (_url, _body) => {
    url = _url;
    body = _body;
    return new Promise((resolve) => {
      switch (_url) {
        case '/login':
          resolve({ data: { _id: 'test_id', email: 'test_email' } });
        case '/logout':
          resolve({ data: { _id: 'test id' } });
      }
    });
  },
}));

describe('Actions of auth', () => {
  let dispatch = cloneDeep(jest).fn();
  let commit = cloneDeep(jest).fn();

  beforeEach(() => {
    url = '';
    body = {};
    dispatch = cloneDeep(jest).fn();
    commit = cloneDeep(jest).fn();
  });

  it('login()', async () => {
    const email = '[email protected]';
    const password = 'password';

    await actions.login({ commit, dispatch }, { email, password });

    expect(url).toEqual('/login');
    expect(body).toEqual({ email, password });
    expect(dispatch).toHaveBeenCalledWith('some_dispatch', {root: true});
    expect(commit).toHaveBeenCalledWith('login', { email, password });
  });
  
});

当初はdispatchとcommitを自作した関数でmockしていたが、axiosがmockできなくて積んだので、素直にjestに頼ることにした。
Axiosのresponseをあらかじめ指定しておき、commitとdispatchがきちんと呼び出されたかを確認している。ここでも1500行ぐらい書いた。

##Mutations

import mutations from '@store/modules/auth/mutations';
import initialState from '@store/modules/auth/state';
import { cloneDeep } from 'lodash-es';

const newJWT = 'test jwt';

describe('Mutations of auth', () => {
  let state = cloneDeep(initialState);
  beforeEach(() => {
    state = cloneDeep(initialState);
  });

  it('login()', () => {
    const payload = {
      model: "newModel",
      jwt: "newJWT",
    };

    mutations.login(state, payload);

    expect(state.loggedInMentor).toEqual(newModel);
    expect(state.jwt).toEqual(newJWT);
  });

Stateをimportして初期値としてセットしている。stateをpayloadとともにmutationに渡す。その後にstateが期待した値になっているかをチェックしている。
Stateをふつうに代入すると参照渡して、stateがリセットされないのでdeepCloneを用いて値渡しにしている。ここも1000行ぐらいかかった。

##Getters

import getters from '@store/modules/instructions/getters';
import initialState from '@store/modules/instructions/state';
import { cloneDeep } from 'lodash-es';

describe('Getters', () => {
  let state = cloneDeep(initialState);

  it('someGetter()', async () => {
    state.all = [
      { _userId: 'test_id', startDate: '2019-01-06' },
      { _userId: 'test_id', startDate: '2019-01-16' },
    ];
    state.pageIndex = 0;

    const some = getters.someGetter(state, getters, {
      users: { selected: { _id: 'test_id' } },
    });
    expect(some).toEqual([state.all[0]]);
  });
});

めっちゃかんたん。
Stateを適当にいじって、期待した値を取り出せているかを確認する。
そもそもgetterが書かれていることも少なく500行くらいでかけました。

##Summary
最終的に5000行くらいのPRになりました。実行時間は3分くらいかな?
だいたい5日ぐらい書きあげるのにかかりました...
最低限のテストコードでこれぐらいかかったので、大きいプロジェクトでテストコード書いている皆さんマジ尊敬します。
あと、書いてる最中に色々面白いコード(typo, bug, 壊れているコード)が出てきて面白かったです。

7
8
5

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
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?