[译] 对 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,快速升职加薪,走上人生巅峰。 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。 

相关文章
相关标签/搜索