Edited at

インターン生が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からちゃんと取得できているかをテスト


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 = '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

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