對 Vue-Router 進(jìn)行單元測試的方法
由于路由通常會把多個組件牽扯到一起操作,所以一般對其的測試都在 端到端/集成 階段進(jìn)行,處于測試金字塔的上層。不過,做一些路由的單元測試還是大有益處的。
對于與路由交互的組件,有兩種測試方式:
- 使用一個真正的 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ǔ)的路由;這讓我們能在單元測試期間更細(xì)粒度的控制應(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> 就會被忽略,不管當(dāng)前路由是什么,渲染的其實都是一個無用的替身組件。
為使用了 mount 的大型渲染樹做些變通
使用 mount 在某些情況下很好,但有時卻是不理想的。比如,當(dāng)渲染整個 <App> 組件時,正趕上渲染樹很大,包含了許多組件,一層層的組件又有自己的子組件。這么些個子組件都要觸發(fā)各種生命周期鉤子、發(fā)起 API 請求什么的。
如果你在用 Jest,其強(qiáng)大的 mock 系統(tǒng)為此提供了一個優(yōu)雅的解決方法。可以簡單的 mock 掉子組件,在本例中也就是 <NestedRoute>。使用了下面的寫法后,以上測試也將能通過:
jest.mock("@/components/NestedRoute.vue", () => ({
name: "NestedRoute",
render: h => h("div")
}))
使用 Mock Router
有時真實路由也不是必要的?,F(xiàn)在升級一下 <NestedRoute>,讓其根據(jù)當(dāng)前 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 并不存在。 我們當(dāng)然可以用一個真正的路由,但在這樣的情況下只用一個 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
比方說當(dāng)路由中包含 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.beforeHooks[0]() 的寫法調(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 常與各種組件互相影響以達(dá)到某些效果,也應(yīng)該做一些集成測試以確保所有事情如預(yù)期般工作。
總結(jié)
本文講述了:
- 測試由 Vue Router 條件渲染的組件
- 用 jest.mock 和 localVue 去 mock Vue 組件
- 從 router 中解耦全局導(dǎo)航 guard 并對其獨立測試
- 用 jest.mock 來 mock 一個模塊
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue3如何將html元素變成canvas(海報生成),進(jìn)行圖片保存/截圖
這篇文章主要介紹了vue3實現(xiàn)將html元素變成canvas(海報生成),進(jìn)行圖片保存/截圖,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05
vue中v-model、v-bind和v-on三大指令的區(qū)別詳解
v-model和v-bind都是數(shù)據(jù)綁定的方式,下面這篇文章主要給大家介紹了關(guān)于vue中v-model、v-bind和v-on三大指令的區(qū)別,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11
在vue中實現(xiàn)iframe嵌套Html頁面及注意事項說明
這篇文章主要介紹了在vue中實現(xiàn)iframe嵌套Html頁面及注意事項說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
vue+vuex+json-seiver實現(xiàn)數(shù)據(jù)展示+分頁功能
這篇文章主要介紹了vue+vuex+json-seiver實現(xiàn)數(shù)據(jù)展示+分頁功能,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-04-04

