そうだ、教祖になろう。出エジプト記 第4章6節 クライアントサイドを自動テストする
Deine Zauber binden wieder,(汝が魔力は再び結び合わせる)
第3章8節 サーバサイドを自動テストするではPythonで書いたLambda処理のユニットテストを作りました。
今回は第4章5節 Veu.jsでクライアントサイドを実装するで実装したVue.jsのコードを自動テストしていきたいと思います。
Vue.jsのユニットテストはJestかMocha+Chaiって感じらしいのですが、よりお手軽なJestを使います。
vue create
がJestをインストールしてくれてtests/unit/sample.spec.js
というテストモジュールができています。
テスト対象のHelloWorld.vue
はすでに消してしまいましたが、試しにテストを起動してみます。
$ yarn test:unit
はい、「んなもんねえよ!」と怒られました。
これを利用してテストモジュールを作ってみます。
名前をtests/unit/Life.spec.js
に変更。
とりあえずで書いたのがこちら。
import axios from 'axios' import Vue from 'vue' import Life from '@/components/Life.vue' import {shallowMount} from '@vue/test-utils' jest.mock('axios') describe('Life.vue', () => { beforeEach(() => { let response = [ 'あなたはアジアの小国の王様に生まれ変わりました。', 'アジアの小国の王様は今から1000年前に生まれました。', '安定した治世で民に敬われながら生き', '40歳で死にました。', ] axios.get.mockResolvedValue(response) }) it('正:疎通', () => { let wrapper = shallowMount(Life, {}) expect(wrapper.find('div#life').exists()).toBe(true) expect(wrapper.find('div#life ul').exists()).toBe(false) }) })
axiosをJestのモックオブジェクトに差し替えてaxios.get
で任意の値を受信するようにしてます。
it()
がテストメソッドですね。
shallowMount
でダミーマウントしたら、生成されたHTMLにタグが存在するかexport().to~()
でアサーションしてます。
beforeEach()
は各テストメソッド実行前に呼ばれるやつです。
これを実行すると、
成功で通りました。
ちなみに、存在しないHTMLタグをアサーションすると、
「そんなのないよー。ぷんぷん。」と優しく怒ってくれます。
実行時になにやら警告が出ています。
(node:5064) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'commit' of undefined (node:5064) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) (node:5064) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. PASS tests/unit/Life.spec.js
Veux系の警告のよう。
そういえば、まだデータストアをモック化してませんでした。
import axios from 'axios' import Vuex from 'vuex' import Life from '@/components/Life.vue' import {shallowMount, createLocalVue} from '@vue/test-utils' const localVue = createLocalVue() localVue.use(Vuex) jest.mock('axios') describe('Life.vue', () => { let store let storeList beforeEach(() => { let response = [ 'あなたはアジアの小国の王様に生まれ変わりました。', 'アジアの小国の王様は今から1000年前に生まれました。', '安定した治世で民に敬われながら生き', '40歳で死にました。', ] axios.get.mockResolvedValue(response) storeList = jest.fn() store = new Vuex.Store({ state: { list: [], }, mutations: { STORE_LIST: storeList }, }) }) it('正:疎通', () => { const wrapper = shallowMount(Life, { store, localVue }) expect(storeList).not.toHaveBeenCalled() expect(wrapper.find('div#life').exists()).toBe(true) expect(wrapper.find('div#life ul').exists()).toBe(false) }) })
データストアを丸々モック化しました。
警告が消えました。
Was die Mode streng geteilt;(時流が強く切り離したものを)
ところで、現在のアサーションは画面表示直後を前提にしています。
mounted()
で呼ばれるaxios
のAjax処理を実行する前ですね。
<ul>
タグもなければMutationも呼ばれてません。
expect(storeList).toHaveBeenCalledTimes(0) expect(wrapper.find('div#life ul').exists()).toBe(false)
Ajax処理が実行されたあとの表示もアサーションしましょう。
Vueの$nextTick()
を使います。
HTMLのDOMが変化するまでまってくれます。
import axios from 'axios' import Vuex from 'vuex' import Life from '@/components/Life.vue' import {shallowMount, createLocalVue} from '@vue/test-utils' const localVue = createLocalVue() localVue.use(Vuex) jest.mock('axios') describe('Life.vue', () => { let store let storeList let wrapper let list beforeEach(() => { list = [ 'あなたはアジアの小国の王子に生まれ変わりました。', 'アジアの小国の王子は今から980年前に生まれました。', '民に敬われた王の嫡男として生き', '50歳で死にました。', ] axios.get.mockResolvedValue({data: list}) storeList = jest.fn((state, payload) => { state.list = payload.list }) store = new Vuex.Store({ state: { list: [], }, mutations: { STORE_LIST: storeList, }, }) }) it('正:mounted', done => { wrapper = shallowMount(Life, { store, localVue, }) expect(storeList).not.toHaveBeenCalled() expect(wrapper.find('div#life').exists()).toBe(true) expect(wrapper.find('div#life ul').exists()).toBe(false) wrapper.vm.$nextTick(() => { expect(storeList).toHaveBeenCalledTimes(1) expect(wrapper.find('div#life ul').exists()).toBe(true) for (let i = 0; i < list.length; i++) { expect( wrapper .findAll('div#life ul li') .at(i) .text() ).toEqual(list[i]) } done() }) })
mounted()
がデータストアのlistにAjax通信の結果を格納したのちは<ul>
タグが現れます。
<ul>
配下の<li>
のテキストを1行ずつアサーションしています。
Alle Menschen werden Brüder,(すべての人々は兄弟となる)
トランジションもアサーションしたいのですが一度にやるとテストケースが長すぎるので、afterEnter()
とafterLeave()
を別々にテストします。
2つテストケースを足しました。
it('正:afterEnter', done => { let mounted = jest.spyOn(Life, 'mounted').mockImplementation(() => {}) wrapper = shallowMount(Life, { data() { return { show: true, } }, store, localVue, }) wrapper.vm.afterEnter() setTimeout(() => { expect(storeList).not.toHaveBeenCalled() expect(wrapper.find('div#life ul').exists()).toBe(false) done() }, 3000) }) it('正:afterLeave', done => { let mounted = jest.spyOn(Life, 'mounted').mockImplementation(() => {}) wrapper = shallowMount(Life, { store, localVue, }) wrapper.vm.afterLeave() setTimeout(() => { expect(storeList).toHaveBeenCalledTimes(1) expect(wrapper.find('div#life ul').exists()).toBe(true) for (let i = 0; i < list.length; i++) { expect( wrapper .findAll('div#life ul li') .at(i) .text() ).toEqual(list[i]) } done() }, 2000) })
いずれもjest.spyOn
でmounted()
をモックで抑止しています。
正:afterEnter
ではshow
がtrue
から始めてafterEnter()
を呼んだあと、一定時間待ってから<ul>
タグがないことアサーションしています。
正:afterLeave
ではshow
がfalse
から始めてafterEnter()
を呼んだあと、一定時間まってから<ul>
タグがあるをアサーションしています。
実行すると、カバレッジ100%になりました。
ちなみにさっきからカバレッジを取得できてるのは--coverage
オプションをpackage.json
に入れたからです。
… "scripts": { … "test:unit": "vue-cli-service test:unit --coverage", … }, …
よくみると、実行結果の1か所が赤くなっています。
PASS tests/unit/Life.spec.js (5.866s)
「いいけど、時間かかりすぎ-」ということです。
5秒以上だと怒られるようです。
実際にsetTimeout
の遅延時間分だけ待っているので当然です。
ので、テスト用にsetTimeout
の遅延時間をいじれるようにします。
テスト対象のsrc/components/Life.vue
で、遅延時間をdata
に入れます。
<template> <div id="life"> <transition name="fade" @after-enter="afterEnter" @after-leave="afterLeave"> <ul v-if="show"> <li v-for="msg in list" :key="msg"> {{ msg }} </li> </ul> </transition> </div> </template> <script> import axios from 'axios' import {mapState} from 'vuex' import * as types from '@/store/mutation-types' const AFTER_ENTER_DELAY = 3000 const AFTER_LEAVE_DELAY = 2000 export default { name: 'Life', data() { return { show: false, delayAfterEnter: AFTER_ENTER_DELAY, delayAfterLeave: AFTER_LEAVE_DELAY, } }, computed: { ...mapState(['list']), }, mounted() { this.rebirth() }, methods: { rebirth() { axios.get('/api/rebirth').then(response => { this.$store.commit(types.STORE_LIST, { list: response.data, }) this.show = true }) }, afterEnter() { var _this = this setTimeout(() => (_this.show = false), this.delayAfterEnter) }, afterLeave() { var _this = this setTimeout(() => _this.rebirth(), this.delayAfterLeave) }, }, } </script> <style lang="stylus"> #life padding 10% li list-style-type none .fade-enter-active, .fade-leave-active transition all .8s ease .fade-enter, .fade-leave-to opacity 0 </style>
ほんで、テストモジュールでdata
の値を差し替えつつ、自分の遅延時間も減らします。
it('正:afterEnter', done => { const AFTER_ENTER_DELAY = 30 let mounted = jest.spyOn(Life, 'mounted').mockImplementation(() => {}) wrapper = shallowMount(Life, { data() { return { show: true, delayAfterEnter: AFTER_ENTER_DELAY, } }, store, localVue, }) wrapper.vm.afterEnter() setTimeout(() => { expect(storeList).not.toHaveBeenCalled() expect(wrapper.find('div#life ul').exists()).toBe(false) done() }, AFTER_ENTER_DELAY) }) it('正:afterLeave', done => { const AFTER_LEAVE_DELAY = 20 let mounted = jest.spyOn(Life, 'mounted').mockImplementation(() => {}) wrapper = shallowMount(Life, { data() { return { delayAfterLeave: AFTER_LEAVE_DELAY, } }, store, localVue, }) wrapper.vm.afterLeave() setTimeout(() => { expect(storeList).toHaveBeenCalledTimes(1) expect(wrapper.find('div#life ul').exists()).toBe(true) for (let i = 0; i < list.length; i++) { expect( wrapper .findAll('div#life ul li') .at(i) .text() ).toEqual(list[i]) } done() }, AFTER_LEAVE_DELAY) })
0.1秒程度に縮まりました。
Wo dein sanfter Flügel weilt.(汝の柔らかな翼が留まる所で)
そういえば、カバレッジがコンソールにしか出ていませんでした。
HTMLにも出力してみます。
package.json
に以下を追加します。
"jest": { … "collectCoverage": true, "collectCoverageFrom": ["src/**/*.{js,vue}"], "coverageReporters": ["text", "html"], "coverageDirectory": "<rootDir>/tests/unit/coverage" }
実行すると、対象ソース全体に対してのカバレッジが表示されます。
tests/unit/coverage/index.html
をプレビューすると、フォルダ/ファイルごとのカバレッジ詳細を確認できます。
いやー、やっぱりユニットテストは苦行ですね。
jest.spyOn()
の利便性を思い出すまで3日掛かりました。
今年中にカバレッジを100%にしたいんだけど、あと1日でできるんだろうか。
無理だった場合は次回が来年になります。
みなさま良いお年を。