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



因爲路由一般會把多個組件牽扯到一塊兒操做,因此通常對其的測試都在 端到端/集成 階段進行,處於測試金字塔的上層。不過,作一些路由的單元測試仍是大有益處的。css

對於與路由交互的組件,有兩種測試方式:html

  • 使用一個真正的 router 實例
  • mock 掉 $route 和 $router 全局對象

由於大多數 Vue 應用用的都是官方的 Vue Router,因此本文會談談這個。vue

建立組件node

咱們會弄一個簡單的 <App> ,包含一個 /nested-child 路由。訪問 /nested-child 則渲染一個 <NestedRoute> 組件。建立 App.vue 文件,並定義以下的最小化組件:webpack

<template>
 <div id="app">
 <router-view />
 </div>
</template>
<script>
export default {
 name: 'app'
}
</script>
複製代碼

<NestedRoute> 一樣迷你:web

<template>
 <div>Nested Route</div>
</template>
<script>
export default {
 name: "NestedRoute"
}
</script>
複製代碼

如今定義一個路由:面試

import NestedRoute from "@/components/NestedRoute.vue"
export default [
 { path: "/nested-route", component: NestedRoute }
]
複製代碼

在真實的應用中,通常會建立一個 router.js 文件並導入定義好的路由,寫出來通常是這樣的:vue-router

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

爲避免調用 Vue.use(...) 污染測試的全局命名空間,咱們將會在測試中建立基礎的路由;這讓咱們能在單元測試期間更細粒度的控制應用的狀態。vue-cli

編寫測試緩存

先看點代碼再說吧。咱們來測試 App.vue ,因此相應的增長一個 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)
 })
})
複製代碼

照例,一開始先把各類模塊引入咱們的測試;尤爲是引入了應用中所需的真實路由。這在某種程度上很理想 -- 若真實路由一旦掛了,單元測試就失敗,這樣咱們就能在部署應用以前修復這類問題。

能夠在 <App> 測試中使用一個相同的 localVue ,並將其聲明在第一個 describe 塊以外。而因爲要爲不一樣的路由作不一樣的測試,因此把 router 定義在 it 塊裏。

另外一個要注意的是這裏用了 mount 而非 shallowMount 。若是用了 shallowMount ,則 <router-link> 就會被忽略,無論當前路由是什麼,渲染的其實都是一個無用的替身組件。

爲使用了 mount 的大型渲染樹作些變通

使用 mount 在某些狀況下很好,但有時倒是不理想的。好比,當渲染整個 <App> 組件時,正遇上渲染樹很大,包含了許多組件,一層層的組件又有本身的子組件。這麼些個子組件都要觸發各類生命週期鉤子、發起 API 請求什麼的。

若是你在用 Jest,其強大的 mock 系統爲此提供了一個優雅的解決方法。能夠簡單的 mock 掉子組件,在本例中也就是 <NestedRoute> 。使用了下面的寫法後,以上測試也將能經過:

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

使用 Mock Router

有時真實路由也不是必要的。如今升級一下 <NestedRoute> ,讓其根據當前 URL 的查詢字符串顯示一個用戶名。此次咱們用 TDD 實現這個特性。如下是一個基礎測試,簡單的渲染了組件並寫了一句斷言:

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>
複製代碼

如今報錯變爲了:

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)
})
複製代碼

這樣測試就能經過了。在本例中,咱們沒有作任何的導航或是和路由的實現相關的任何其餘東西,因此 mocks 就挺好。咱們並不真的關心 username 是從查詢字符串中怎麼來的,只要它出現就好。

測試路由鉤子的策略

Vue Router 提供了多種類型的路由鉤子, 稱爲 「navigation guards」。舉兩個例子如:

  • 全局 guards ( router.beforeEach )。在 router 實例上聲明
  • 組件內 guards,好比 beforeRouteEnter 。在組件中聲明

要確保這些運做正常,通常是集成測試的工做,由於須要一個使用者從一個理由導航到另外一個。但也能夠用單元測試檢驗導航 guards 中調用的函數是否正常工做,並更快的得到潛在錯誤的反饋。這裏列出一些如何從導航 guards 中解耦邏輯的策略,以及爲此編寫的單元測試。

全局 guards

比方說當路由中包含 shouldBustCache 元數據的狀況下,有那麼一個 bustCache 函數就應該被調用。路由可能長這樣:

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

之因此使用 shouldBustCache 元數據,是爲了讓緩存無效,從而確保用戶不會取得舊數據。一種可能的實現以下:

//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
複製代碼

在單元測試中,你可能想導入 router 實例,並試圖經過 router.beforeHooks[0]() 的寫法調用 beforeEach ;但這將拋出一個關於 next 的錯誤 -- 由於無法傳入正確的參數。針對這個問題,一種策略是在將 beforeEach 導航鉤子耦合到路由中以前,解耦並單獨導出它。作法是這樣的:

//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 鉤子將其復原。經過將 beforeEach 導出爲一個已結耦的、普通的 Javascript 函數,從而讓其在測試中不成問題。

爲了肯定 hook 真的調用了 bustCache 而且顯示了最新的數據,可使用一個諸如 Cypress.io 的端到端測試工具,它也在應用腳手架 vue-cli 的選項中提供了。

組件 guards

一旦將組件 guards 視爲已結耦的、普通的 Javascript 函數,則它們也是易於測試的。假設咱們爲 <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()
})
複製代碼

這樣的單元測試行之有效,能夠在開發過程當中當即獲得反饋;但因爲路由和導航 hooks 常與各類組件互相影響以達到某些效果,也應該作一些集成測試以確保全部事情如預期般工做。

總結

本次給你們推薦一個免費的學習羣,裏面歸納移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同窗,歡迎加入Q羣:582735936,無論你是小白仍是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時天天更新視頻資料。 最後,祝你們早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峯。 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。 

相關文章
相關標籤/搜索