1. 程式人生 > >[譯] 對 Vue-Router 進行單元測試

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

圖片描述(最多50字)

由於路由通常會把多個元件牽扯到一起操作,所以一般對其的測試都在 端到端/整合 階段進行,處於測試金字塔的上層。不過,做一些路由的單元測試還是大有益處的。

對於與路由互動的元件,有兩種測試方式:

使用一個真正的 router 例項
mock 掉 $route 和 $router 全域性物件
因為大多數 Vue 應用用的都是官方的 Vue Router,所以本文會談談這個。

建立元件

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

<template>
  <div id="app">
    <router-view />
  </div>
</template>
<script>
export default {
  name: 'app'
}
同樣迷你:
<template>
  <div>Nested Route</div>
</template>
<script>
export default {
  name: "NestedRoute"
}
現在定義一個路由:
import NestedRoute from "@/components/NestedRoute.vue"
export default [
  { path: "/nested-route", component: NestedRoute }
]
在真實的應用中,一般會建立一個 router.js   檔案並匯入定義好的路由,寫出來一般是這樣的:

import Vue from "vue"
import VueRouter from "vue-router"
import routes from "./routes.js"
Vue.use(VueRouter)
export default new VueRouter({ routes })
為避免呼叫 Vue.use(...)   汙染測試的全域性名稱空間,我們將會在測試中建立基礎的路由;這讓我們能在單元測試期間更細粒度的控制應用的狀態。

編寫測試

先看點程式碼再說吧。我們來測試 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.beforeHooks0 的寫法呼叫 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()
  }
}
對在全域性 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,快速升職加薪,走上人生巔峰。

簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。