[譯] 對 Vue-Router 進行單元測試

由于路由通常會把多個組件牽扯到一起操作曹抬,所以一般對其的測試都在 端到端/集成 階段進行材部,處于測試金字塔的上層。不過重斑,做一些路由的單元測試還是大有益處的。

對于與路由交互的組件肯骇,有兩種測試方式:

使用一個真正的 router 實例
mock 掉 route 和router 全局對象
因為大多數(shù) Vue 應(yīng)用用的都是官方的 Vue Router窥浪,所以本文會談?wù)勥@個。

創(chuàng)建組件

我們會弄一個簡單的 <App> 笛丙,包含一個 /nested-child 路由漾脂。訪問 /nested-child 則渲染一個 <NestedRoute> 組件。創(chuàng)建 App.vue 文件胚鸯,并定義如下的最小化組件:

<template>
 <div id="app">
 <router-view />
 </div>
</template>
<script>
export default {
 name: 'app'
}
</script>
<NestedRoute> 同樣迷你:

<template>
 <div>Nested Route</div>
</template>
<script>
export default {
 name: "NestedRoute"
}```
</script>
現(xiàn)在定義一個路由:
import NestedRoute from "@/components/NestedRoute.vue"
export default [
 { path: "/nested-route", component: NestedRoute }
]```

在真實的應(yīng)用中骨稿,一般會創(chuàng)建一個 router.js 文件并導(dǎo)入定義好的路由,寫出來一般是這樣的:

import Vue from "vue"
import VueRouter from "vue-router"
import routes from "./routes.js"
Vue.use(VueRouter)
export default new VueRouter({ routes })

為避免調(diào)用 Vue.use(...) 污染測試的全局命名空間姜钳,我們將會在測試中創(chuàng)建基礎(chǔ)的路由坦冠;這讓我們能在單元測試期間更細粒度的控制應(yīng)用的狀態(tài)。

編寫測試

先看點代碼再說吧哥桥。我們來測試 App.vue 蓝牲,所以相應(yīng)的增加一個 App.spec.js :

import { shallowMount, mount, createLocalVue } from "@vue/test-utils"
import App from "@/App.vue"
import VueRouter from "vue-router"
import NestedRoute from "@/components/NestedRoute.vue"
import routes from "@/routes.js"
const localVue = createLocalVue()
localVue.use(VueRouter)
describe("App", () => {
 it("renders a child component via routing", () => {
 const router = new VueRouter({ routes })
 const wrapper = mount(App, { localVue, router })
 router.push("/nested-route")
 expect(wrapper.find(NestedRoute).exists()).toBe(true)
 })
})

照例,一開始先把各種模塊引入我們的測試泰讽;尤其是引入了應(yīng)用中所需的真實路由。這在某種程度上很理想 -- 若真實路由一旦掛了,單元測試就失敗已卸,這樣我們就能在部署應(yīng)用之前修復(fù)這類問題佛玄。

可以在 <App> 測試中使用一個相同的 localVue ,并將其聲明在第一個 describe 塊之外累澡。而由于要為不同的路由做不同的測試梦抢,所以把 router 定義在 it 塊里。

另一個要注意的是這里用了 mount 而非 shallowMount 愧哟。如果用了 shallowMount 奥吩,則 <router-link> 就會被忽略,不管當前路由是什么蕊梧,渲染的其實都是一個無用的替身組件霞赫。

為使用了 mount 的大型渲染樹做些變通

使用 mount 在某些情況下很好,但有時卻是不理想的肥矢。比如端衰,當渲染整個 <App> 組件時,正趕上渲染樹很大甘改,包含了許多組件旅东,一層層的組件又有自己的子組件。這么些個子組件都要觸發(fā)各種生命周期鉤子十艾、發(fā)起 API 請求什么的抵代。

如果你在用 Jest,其強大的 mock 系統(tǒng)為此提供了一個優(yōu)雅的解決方法忘嫉』珉梗可以簡單的 mock 掉子組件,在本例中也就是 <NestedRoute> 榄融。使用了下面的寫法后参淫,以上測試也將能通過:

jest.mock("@/components/NestedRoute.vue", () => ({
 name: "NestedRoute",
 render: h => h("div")
}))

使用 Mock Router

有時真實路由也不是必要的。現(xiàn)在升級一下 <NestedRoute> 愧杯,讓其根據(jù)當前 URL 的查詢字符串顯示一個用戶名涎才。這次我們用 TDD 實現(xiàn)這個特性。以下是一個基礎(chǔ)測試力九,簡單的渲染了組件并寫了一句斷言:

import { shallowMount } from "@vue/test-utils"
import NestedRoute from "@/components/NestedRoute.vue"
import routes from "@/routes.js"
describe("NestedRoute", () => {
 it("renders a username from query string", () => {
 const username = "alice"
 const wrapper = shallowMount(NestedRoute)
 expect(wrapper.find(".username").text()).toBe(username)
 })
})

然而我們并沒有 <div class="username"> 耍铜,所以一運行測試就會報錯:

tests/unit/NestedRoute.spec.js
 NestedRoute
 ? renders a username from query string (25ms)
 ● NestedRoute ? renders a username from query string
 [vue-test-utils]: find did not return .username, cannot call text() on empty Wrapper
來更新一下 <NestedRoute> :

<template>
 <div>
 Nested Route
 <div class="username">
 {{ $route.params.username }}
 </div>
 </div>
</template>

現(xiàn)在報錯變?yōu)榱耍?/p>

tests/unit/NestedRoute.spec.js
 NestedRoute
 ? renders a username from query string (17ms)
 ● NestedRoute ? renders a username from query string
 TypeError: Cannot read property 'params' of undefined

這是因為 $route 并不存在。 我們當然可以用一個真正的路由跌前,但在這樣的情況下只用一個 mocks 加載選項會更容易些:

it("renders a username from query string", () => {
 const username = "alice"
 const wrapper = shallowMount(NestedRoute, {
 mocks: {
 $route: {
 params: { username }
 }
 }
 })
 expect(wrapper.find(".username").text()).toBe(username)
})

這樣測試就能通過了棕兼。在本例中,我們沒有做任何的導(dǎo)航或是和路由的實現(xiàn)相關(guān)的任何其他東西抵乓,所以 mocks 就挺好伴挚。我們并不真的關(guān)心 username 是從查詢字符串中怎么來的靶衍,只要它出現(xiàn)就好。

測試路由鉤子的策略

Vue Router 提供了多種類型的路由鉤子, 稱為 “navigation guards”茎芋。舉兩個例子如:

全局 guards ( router.beforeEach )颅眶。在 router 實例上聲明
組件內(nèi) guards,比如 beforeRouteEnter 田弥。在組件中聲明
要確保這些運作正常涛酗,一般是集成測試的工作,因為需要一個使用者從一個理由導(dǎo)航到另一個偷厦。但也可以用單元測試檢驗導(dǎo)航 guards 中調(diào)用的函數(shù)是否正常工作商叹,并更快的獲得潛在錯誤的反饋。這里列出一些如何從導(dǎo)航 guards 中解耦邏輯的策略只泼,以及為此編寫的單元測試剖笙。

全局 guards

比方說當路由中包含 shouldBustCache 元數(shù)據(jù)的情況下,有那么一個 bustCache 函數(shù)就應(yīng)該被調(diào)用辜妓。路由可能長這樣:

//routes.js
import NestedRoute from "@/components/NestedRoute.vue"
export default [
 {
 path: "/nested-route",
 component: NestedRoute,
 meta: {
 shouldBustCache: true
 }
 }
]

之所以使用 shouldBustCache 元數(shù)據(jù)枯途,是為了讓緩存無效,從而確保用戶不會取得舊數(shù)據(jù)籍滴。一種可能的實現(xiàn)如下:

//router.js
import Vue from "vue"
import VueRouter from "vue-router"
import routes from "./routes.js"
import { bustCache } from "./bust-cache.js"
Vue.use(VueRouter)
const router = new VueRouter({ routes })
router.beforeEach((to, from, next) => {
 if (to.matched.some(record => record.meta.shouldBustCache)) {
 bustCache()
 }
 next()
})
export default router

在單元測試中酪夷,你可能想導(dǎo)入 router 實例,并試圖通過 router.beforeHooks0 的寫法調(diào)用 beforeEach 孽惰;但這將拋出一個關(guān)于 next 的錯誤 -- 因為沒法傳入正確的參數(shù)晚岭。針對這個問題,一種策略是在將 beforeEach 導(dǎo)航鉤子耦合到路由中之前勋功,解耦并單獨導(dǎo)出它坦报。做法是這樣的:

//router.js
export function beforeEach((to, from, next) {
 if (to.matched.some(record => record.meta.shouldBustCache)) {
 bustCache()
 }
 next()
}
router.beforeEach((to, from, next) => beforeEach(to, from, next))
export default router

再寫測試就容易了,雖然寫起來有點長:

import { beforeEach } from "@/router.js"
import mockModule from "@/bust-cache.js"
jest.mock("@/bust-cache.js", () => ({ bustCache: jest.fn() }))
describe("beforeEach", () => {
 afterEach(() => {
 mockModule.bustCache.mockClear()
 })
 it("busts the cache when going to /user", () => {
 const to = {
 matched: [{ meta: { shouldBustCache: true } }]
 }
 const next = jest.fn()
 beforeEach(to, undefined, next)
 expect(mockModule.bustCache).toHaveBeenCalled()
 expect(next).toHaveBeenCalled()
 })
 it("busts the cache when going to /user", () => {
 const to = {
 matched: [{ meta: { shouldBustCache: false } }]
 }
 const next = jest.fn()
 beforeEach(to, undefined, next)
 expect(mockModule.bustCache).not.toHaveBeenCalled()
 expect(next).toHaveBeenCalled()
 })
})

最主要的有趣之處在于狂鞋,我們借助 jest.mock 片择,mock 掉了整個模塊,并用 afterEach 鉤子將其復(fù)原骚揍。通過將 beforeEach 導(dǎo)出為一個已結(jié)耦的字管、普通的 Javascript 函數(shù),從而讓其在測試中不成問題信不。

為了確定 hook 真的調(diào)用了 bustCache 并且顯示了最新的數(shù)據(jù)嘲叔,可以使用一個諸如 Cypress.io 的端到端測試工具,它也在應(yīng)用腳手架 vue-cli 的選項中提供了抽活。

組件 guards

一旦將組件 guards 視為已結(jié)耦的硫戈、普通的 Javascript 函數(shù),則它們也是易于測試的下硕。假設(shè)我們?yōu)?<NestedRoute> 添加了一個 beforeRouteLeave hook:

//NestedRoute.vue
<script>
import { bustCache } from "@/bust-cache.js"
export default {
 name: "NestedRoute",
 beforeRouteLeave(to, from, next) {
 bustCache()
 next()
 }
}

</script>
對在全局 guard 中的方法照貓畫虎就可以測試它了:

// ...
import NestedRoute from "@/compoents/NestedRoute.vue"
import mockModule from "@/bust-cache.js"
jest.mock("@/bust-cache.js", () => ({ bustCache: jest.fn() }))
it("calls bustCache and next when leaving the route", () => {
 const next = jest.fn()
 NestedRoute.beforeRouteLeave(undefined, undefined, next)
 expect(mockModule.bustCache).toHaveBeenCalled()
 expect(next).toHaveBeenCalled()
})

這樣的單元測試行之有效丁逝,可以在開發(fā)過程中立即得到反饋汁胆;但由于路由和導(dǎo)航 hooks 常與各種組件互相影響以達到某些效果,也應(yīng)該做一些集成測試以確保所有事情如預(yù)期般工作霜幼。

總結(jié)

本次給大家推薦一個免費的學(xué)習(xí)群沦泌,里面概括移動應(yīng)用網(wǎng)站開發(fā),css辛掠,html,webpack释牺,vue node angular以及面試資源等萝衩。
對web開發(fā)技術(shù)感興趣的同學(xué),歡迎加入Q群:582735936没咙,不管你是小白還是大牛我都歡迎猩谊,還有大牛整理的一套高效率學(xué)習(xí)路線和教程與您免費分享,同時每天更新視頻資料祭刚。
最后牌捷,祝大家早日學(xué)有所成,拿到滿意offer涡驮,快速升職加薪暗甥,走上人生巔峰。

簡書著作權(quán)歸作者所有捉捅,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處撤防。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市棒口,隨后出現(xiàn)的幾起案子寄月,更是在濱河造成了極大的恐慌,老刑警劉巖无牵,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漾肮,死亡現(xiàn)場離奇詭異,居然都是意外死亡茎毁,警方通過查閱死者的電腦和手機克懊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來充岛,“玉大人保檐,你說我怎么就攤上這事〈薰#” “怎么了夜只?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蒜魄。 經(jīng)常有香客問我扔亥,道長场躯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任旅挤,我火速辦了婚禮踢关,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粘茄。我一直安慰自己签舞,他們只是感情好,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布柒瓣。 她就那樣靜靜地躺著儒搭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芙贫。 梳的紋絲不亂的頭發(fā)上搂鲫,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機與錄音磺平,去河邊找鬼魂仍。 笑死,一個胖子當著我的面吹牛拣挪,可吹牛的內(nèi)容都是我干的擦酌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼媒吗,長吁一口氣:“原來是場噩夢啊……” “哼仑氛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起闸英,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锯岖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后甫何,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體出吹,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年辙喂,在試婚紗的時候發(fā)現(xiàn)自己被綠了捶牢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡巍耗,死狀恐怖秋麸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炬太,我是刑警寧澤灸蟆,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站亲族,受9級特大地震影響炒考,放射性物質(zhì)發(fā)生泄漏可缚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一斋枢、第九天 我趴在偏房一處隱蔽的房頂上張望帘靡。 院中可真熱鬧,春花似錦瓤帚、人聲如沸描姚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轰胁。三九已至,卻和暖如春朝扼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霎肯。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工擎颖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人观游。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓搂捧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親懂缕。 傳聞我的和親對象是個殘疾皇子允跑,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容