直接使用 jest.mock()

若在 Component 使用 Vuex 的 Action,對 Component 進行 Unit Test 時,就必須對 Action 加以 Mock。

Version


Vue 2.5.22
Vuex 3.0.1
Vue-test-utils 1.0.0-beta.20

Store


books-info.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fetchBooks } from '@/api/books';

/** mutation */
const setBooks = (state, { books }) => state.books = books;

/** action */
const saveBooks = commit => res => commit('setBooks', res.data);
const loadBooks = ({ commit }) => fetchBooks().then(saveBooks(commit));

/** getter */
const booksCount = ({ books }) => books.length;

export default {
namespaced: true,
state: {
books: []
},
mutations: {
setBooks
},
getters: {
booksCount
},
actions: {
loadBooks
},
};

一個典型的 Vuex store,包含 statemutationsgettersactions

24 行

1
2
3
actions: { 
loadBooks
},

宣告 loadBooks action。

Component


book-list.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
<ul>
<li v-for="(item, index) in books" :key="index">
Title : {{ item.title }}, Price : {{ item.price }}
</li>
</ul>
<p>Count: {{ booksCount }}</p>
</div>
</template>

<script>
import store from '../store/index';

/** All books */
const books = () => store.state.BooksInfo.books;

/** Books count */
const booksCount = () => store.getters['BooksInfo/booksCount'];

/** Load all books */
const mounted = () => store.dispatch('BooksInfo/loadBooks');

export default {
name: 'BookList',
computed: {
books,
booksCount
},
mounted,
}
</script>

一個典型的 component,包含 mountedcomputed

13 行

1
import store from '@/store/index';

因為 component 要使用 Vuex 的 Store,因此將 store import 進來。

傳統在 Vue 使用 Vuex,都是將 Store 掛在 Vue Instance 下,然後使用 this.$store 存取 Store,但這種方式太依賴 this,導致 Vue 必須使用 Function Declaration 寫法,因為這種寫法才能讓 Vue 底層透過 bind() 修改 this;但一旦使用了 Function Declaration,若 function 重複需要重構,由於有 this,就只能走向 Vue 的 Mixin 一途

若 function 能不使用 this,就能改用 ES2015 的 Arrow Function 寫法,重構就抽成 ES2015 的 module 再 import 進來即可,這種方式較符合 ES2015 module 精神;也由於 function 都沒有 this,沒有任何 Side Effect,更方便實踐 Higher Order Function 、Closure 與 Function Composition,符合 Functional Programming 精神

main.js

1
2
3
4
5
6
7
8
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')

由於沒將 store 掛在 Vue Instance 下,因此 main.js 也不用 Vue.use(Vuex),也就是 main.js 完全不需修改。

15 行

1
2
/** All books */
const books = () => store.state.BooksInfo.books;

Component 沒有 data,全部由 Vuex 的 state 而來。

注意並沒有使用 this.$store 存取 Store,而是直接用 import 的 store 去存取,且沒使用 Function Declaration,直接使用 Arraw Function

18 行

1
2
/** Books count */
const booksCount = () => store.getters['BooksInfo/booksCount'];

booksCount computed 由 Vuex 的 getter 而來。

注意並沒有使用 this.$store 存取 Store,而是直接用 import 的 store 去存取,且沒使用 Function Declaration,直接使用 Arraw Function

21 行

1
2
/** Load all books */
const mounted = () => store.dispatch('BooksInfo/loadBooks');

mounted 去呼叫 Vuex 的 action

注意並沒有使用 this.$store 存取 Store,而是直接用 import 的 store 去存取,且沒使用 Function Declaration,直接使用 Arraw Function

Unit Test


component-action.spec.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { shallowMount, createLocalVue } from '@vue/test-utils';
import store from '@/store/index';
import BookList from '@/components/book-list.vue';

test('action', () => {
/** arrange */
store.dispatch = jest.fn();

/** act */
const localVue = createLocalVue();
shallowMount(BookList, { localVue });

/** assert */
expect(store.dispatch).toHaveBeenCalled();
});

由於 component 並不是使用 this.$store 去存取 Store,因此 Vue Test Utils 或網路上很多教學的 Mock Vuex 的方法並不適用,因為它們都是基於對 this.$store 建立 Fake Store,但我們現在已經不是使用 this.$store,而是直接 import store module,所以我們要做的是直接 mock store.dispatch()

第 1 行

1
import { shallowMount, createLocalVue } from '@vue/test-utils';

直接 import Vue Test Utiles 兩個最重要的 shallowMount()createLocalVue(),稍後會說明。

第 2 行

1
import store from '@/store/index';

因為要直接對 store.dispatch() 做 mock,因此要特別 import 進來。

第 3 行

1
import BookList from '@/components/book-list.vue';

將我們要測試的 BookList component import 進來。

第 5 行

1
2
3
4
5
6
7
test('action', () => {
/** arrange */

/** act */

/** assert */
});

所有的 unit test 都包在 test() 的第二個參數,以 Arrow Function 表示。

test() 的第一個參數為 description,可描述 test case。

一樣使用 3A 原則寫 unit test。

第 6 行

1
2
/** arrange */
store.dispatch = jest.fn();

由於 mounted() hook 會執行 store.dispatch('BooksInfo/loadBooks');,因此只要使用 jest.fn()store.dispatch() 進行 mock,再測試 store.dispatch() 有沒有被執行過即可。

第 9 行

1
2
3
/** act */
const localVue = createLocalVue();
shallowMount(BookList, { localVue });

使用 Vue Test Utils 的 createLocalVue() 建立測試用的 Vue instance。

使用 shallowMount() 建立假 component 測試,並將 localVue 傳入。

13 行

1
2
/** assert */
expect(store.dispatch).toHaveBeenCalled();

直接驗證 store.dispatch() 有沒有被執行過。

1
$ yarn test:unit

mock003

通過 unit test 得到 綠燈

Conclusion


  • 若直接將 store import 進 component,而非使用 this.$store,則 mock state 的方式會有所不同,必須使用 jest.mock() 直接對 store module 進行 mock
  • 使用 store import 寫法,可避免 this.$store 大量依賴 this,造成日後重構困難,且可大大提升 Vue 使用 Higher Order Function、Closure 與 Function Composition 能力
  • jest.mock() 會 Hoisting 到所有 import 前面,若在 factory function 需 reference 其他module,必須使用 require(),不能使用 import

Sample Code


完整範例可以在我的 GitHub 上找到

Reference


Jest, ES6 Class Mocks