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

ある日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からちゃんと取得できているかをテスト

```js: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ができないできない。

```js: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できないこと。
調べてみると[このような記事](https://github.com/vuejs/vue-test-utils/issues/323)が出てきて、ちゃんとテストはできるんです。
しかし、storeとかと両立しようとした途端にダメになる。もうstoreインポートした時点でおじゃんですよおじゃん。両立しているコードはネットの海の中で見つけられませんでした。だれか強いひと教えてください。
仕方がないので、上記のようにtry and catchをしてエラーコードが期待された値になっているかで判定することにしました。

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

##Actions

```js
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 = 'test@gmail.com';
    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

```js
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

```js
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, 壊れているコード)が出てきて面白かったです。