淺談Vue組件單元測(cè)試究竟測(cè)試什么
關(guān)于 Vue 組件單元測(cè)試最常見的問題就是“我究竟應(yīng)該測(cè)試什么?”
雖然測(cè)試過(guò)多或過(guò)少都是可能的,但我的觀察是,開發(fā)人員通常會(huì)測(cè)試過(guò)頭。畢竟,沒有人愿意自己的組件未經(jīng)測(cè)試從而導(dǎo)致應(yīng)用程序在生產(chǎn)中崩潰。
在本文中,我將分享一些用于組件單元測(cè)試的指導(dǎo)原則,這些指導(dǎo)原則可以確保在編寫測(cè)試上不會(huì)花費(fèi)大量時(shí)間,但是可以提供足夠的覆蓋率來(lái)避免錯(cuò)誤。
本文假設(shè)你已經(jīng)了解 Jest 和 Vue Test Utils。
示例組件
在學(xué)習(xí)這些指導(dǎo)原則之前,我們先來(lái)熟悉下要測(cè)試的示例組件。組件名為 Item.vue ,是 eCommerce App 里的一個(gè)產(chǎn)品條目。

下面是組件的源碼。注意有三個(gè)依賴項(xiàng):Vuex ( $store ), Vue Router ( $router ) 和 Vue Auth ( $auth )。
Item.vue
<template>
<div>
<h2>{{ item.title }}</h2>
<button @click="addToCart">Add To Cart</button>
<img :src="item.image"/>
</div>
</template>
<script>
export default {
name: "Item",
props: [ "id" ],
computed: {
item () {
return this.$store.state.find(
item => item.id === this.id
);
}
},
methods: {
addToCart () {
if (this.$auth.check()) {
this.$store.commit("ADD_TO_CART", this.id);
} else {
this.$router.push({ name: "login" });
}
}
}
};
</script>
配置 Spec 文件
下面是測(cè)試用的 spec 文件。其中,我們將用 Vue Test Utils “淺掛載”示例組件,因此引入了相關(guān)模塊以及我們要測(cè)試的 Item 組件。
同時(shí)還寫了一個(gè)工廠函數(shù)用于生成可覆蓋的配置對(duì)象,以免在每個(gè)測(cè)試中都需要指定 props 和 mock 三個(gè)依賴項(xiàng)。 item.spec.js
import { shallowMount } from "@vue/test-utils";
import Item from "@/components/Item";
function createConfig (overrides) {
const id = 1;
const mocks = {
// Vue Auth
$auth: {
check: () => false
},
// Vue Router
$router: {
push: () => {}
},
// Vuex
$store: {
state: [ { id } ],
commit: () => {}
}
};
const propsData = { id };
return Object.assign({ mocks, propsData }, overrides);
}
describe("Item.vue", () => {
// Tests go here
});
確定業(yè)務(wù)邏輯
對(duì)于要測(cè)試的組件,要問的第一個(gè)也是最重要的問題是“業(yè)務(wù)邏輯是什么”,即組件是做什么的?
對(duì)于這個(gè) Item.vue ,業(yè)務(wù)邏輯是:
- 根據(jù)接收的id屬性展示條目信息
- 如果用戶是訪客,點(diǎn)擊 Add to Cart 按鈕將重定向到登錄頁(yè)
- 如果用戶已登錄,點(diǎn)擊 Add to Cart 按鈕會(huì)觸發(fā) Vuex mutation ADD_TO_CART。
確定輸入和輸出
當(dāng)你對(duì)組件做單元測(cè)試時(shí),可將其視為一個(gè)黑盒。方法、計(jì)算屬性等內(nèi)部邏輯只影響輸出。
因此,下一個(gè)重點(diǎn)是確定組件的輸入和輸出,因?yàn)檫@些也是測(cè)試的輸入和輸出。
Item.vue 的輸入是:
- id 屬性
- 來(lái)自 Vuex 和 Vue Auth 的數(shù)據(jù)狀態(tài)
- 用戶點(diǎn)擊按鈕
輸出是:
- 渲染后的 HTML
- 發(fā)送到 Vuex mutation 或者 Vue Router push 的數(shù)據(jù)
有些組件也會(huì)將表單和事件作為輸入,觸發(fā)事件作為輸出。
測(cè)試 1: 訪客點(diǎn)擊按鈕跳轉(zhuǎn)路由
有一個(gè)業(yè)務(wù)邏輯是“如果用戶是訪客,點(diǎn)擊 Add to Cart 按鈕將重定向到登錄頁(yè)”。我們來(lái)寫這個(gè)測(cè)試。
我們通過(guò)“shallow mount”組件來(lái)編寫測(cè)試,然后找到并點(diǎn)擊 Add to Cart 按鈕。
test("router called when guest clicks button", () => {
const config = createConfig();
const wrapper = shallowMount(Item, config);
wrapper
.find("button")
.trigger("click");
// Assertion goes here
}
隨后我們會(huì)加上 assertion。
不要超出輸入和輸出的界限
在這個(gè)測(cè)試中很容易采取的做法是在點(diǎn)擊按鈕后判斷路由是否跳轉(zhuǎn)到了登錄頁(yè),比如:
import router from "router";
test("router called when guest clicks button", () => {
...
// 錯(cuò)!
const route = router.find(route => route.name === "login");
expect(wrapper.vm.$route.path).toBe(route.path);
}
雖然這確實(shí)也能測(cè)試組件的輸出,但是它依賴于路由功能,這不應(yīng)該是組件所關(guān)心的。
直接測(cè)試組件的輸出會(huì)更好,也就是調(diào)用了 $router.push 。至于路由是否最終完成了操作,這已經(jīng)超出了本測(cè)試的范疇。
因此我們可以監(jiān)聽路由的 push 方法,并斷言它是否被登錄路由對(duì)象調(diào)用。
import router from "router";
test("router called when guest clicks button", () => {
...
jest.spyOn(config.mocks.$router, "push");
const route = router.find(route => route.name === "login");
expect(spy).toHaveBeenCalledWith(route);
}
測(cè)試 2: 登錄用戶點(diǎn)擊按鈕后調(diào)用 vuex
接下來(lái)讓我們測(cè)試業(yè)務(wù)邏輯“如果用戶已登錄,點(diǎn)擊 Add to Cart 按鈕將觸發(fā) Vuex mutation ADD_TO_CART ”。
同樣,你不需要判斷 Vuex 狀態(tài)是否更改了。要驗(yàn)證這個(gè)需要另外單獨(dú)測(cè)試 Vuex store。
組件的職責(zé)只是執(zhí)行 commit,因此我們只要測(cè)試這個(gè)動(dòng)作就行。
首先重寫 $auth.check 假數(shù)據(jù)讓它返回 true (模擬登錄用戶)。然后監(jiān)聽 store 的 commit 方法,并斷言點(diǎn)擊按鈕后被調(diào)用。
test("vuex called when auth user clicks button", () => {
const config = createConfig({
mocks: {
$auth: {
check: () => true
}
}
});
const spy = jest.spyOn(config.mocks.$store, "commit");
const wrapper = shallowMount(Item, config);
wrapper
.find("button")
.trigger("click");
expect(spy).toHaveBeenCalled();
}
不要測(cè)試其他庫(kù)的功能
Item 組件展示條目數(shù)據(jù),特別是標(biāo)題和圖片?;蛟S我們應(yīng)該寫一個(gè)測(cè)試來(lái)專門檢查這些?比如:
test("renders correctly", () => {
const wrapper = shallowMount(Item, createConfig());
// Wrong
expect(wrapper.find("h2").text()).toBe(item.title);
}
這又是一個(gè)不必要的測(cè)試,因?yàn)樗皇菧y(cè)試了 Vue 從 Vuex 中提取數(shù)據(jù)并插入到模板的能力。Vue 這個(gè)庫(kù)已經(jīng)對(duì)該機(jī)制進(jìn)行了測(cè)試,所以你應(yīng)該依賴于它。
測(cè)試 3: 正確地渲染
但是等等,如果有人不小心將 title 重命名為 name ,然后忘記更新插值表達(dá)式怎么辦?這難道不需要測(cè)試嗎?
沒錯(cuò),但是如果你像這樣來(lái)測(cè)試模板的方方面面,何時(shí)才是個(gè)頭?
測(cè)試 HTML 最好的辦法是使用快照,用來(lái)檢查整體渲染后的結(jié)果。這不僅覆蓋了標(biāo)題插值,還包括圖片、按鈕文本、任何 class 等。
test("renders correctly", () => {
const wrapper = shallowMount(Item, createConfig());
expect(wrapper).toMatchSnapshot();
});
其他不需要測(cè)試的點(diǎn)還有這些:
- src 屬性是否綁定到 img 元素
- 添加到 Vuex store 中的數(shù)據(jù)是否跟插入的數(shù)據(jù)一致
- 計(jì)算屬性是否返回了正確的數(shù)據(jù)
- 執(zhí)行 router push 是否重定向到正確的頁(yè)面
諸如此類。
總結(jié)
我認(rèn)為上面三個(gè)簡(jiǎn)單的測(cè)試對(duì)這個(gè)組件來(lái)說(shuō)足夠了。
組件單元測(cè)試的一個(gè)好理念是先假設(shè)測(cè)試是不必要的,除非被證明是必要的。
你可以問自己以下問題:
- 這是業(yè)務(wù)邏輯的一部分嗎?
- 這是直接測(cè)試組件的輸入和輸出嗎?
- 這是測(cè)試自己的代碼,還是第三方代碼?
讓我們愉快地單元測(cè)試吧!希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Vue-router路由判斷頁(yè)面未登錄跳轉(zhuǎn)到登錄頁(yè)面的實(shí)例
下面小編就為大家?guī)?lái)一篇Vue-router路由判斷頁(yè)面未登錄跳轉(zhuǎn)到登錄頁(yè)面的實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10
vue keep-alive實(shí)現(xiàn)多組件嵌套中個(gè)別組件存活不銷毀的操作
這篇文章主要介紹了vue keep-alive實(shí)現(xiàn)多組件嵌套中個(gè)別組件存活不銷毀的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
詳解為什么Vue中不要用index作為key(diff算法)
這篇文章主要介紹了詳解為什么Vue中不要用index作為key(diff算法),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04
vue項(xiàng)目開發(fā)中setTimeout等定時(shí)器的管理問題
這篇文章主要介紹了vue項(xiàng)目開發(fā)中setTimeout等定時(shí)器的管理問題,需要的朋友可以參考下2018-09-09
vue項(xiàng)目實(shí)現(xiàn)按鈕可隨意移動(dòng)
這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目實(shí)現(xiàn)按鈕可隨意移動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
fullcalendar日程管理插件月份切換回調(diào)處理方案
這篇文章主要為大家介紹了fullcalendar日程管理插件月份切換回調(diào)處理的方案示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
使用Vue創(chuàng)建前后端分離項(xiàng)目的完整過(guò)程(前端部分)
這篇文章主要介紹了使用Vue.js和Node.js搭建一個(gè)前端和后端分離的項(xiàng)目,并使用VueCLI3腳手架、axios進(jìn)行HTTP請(qǐng)求、Vue-router實(shí)現(xiàn)前端路由和vuex進(jìn)行狀態(tài)管理,需要的朋友可以參考下2025-01-01
詳解使用VueJS開發(fā)項(xiàng)目中的兼容問題
這篇文章主要介紹了詳解使用VueJS開發(fā)項(xiàng)目中的兼容問題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08

