ある日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からちゃんと取得できているかをテスト
"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ができないできない。
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, 壊れているコード)が出てきて面白かったです。