对 Vue-Router 进行单元测试的方法


Posted in Javascript onNovember 05, 2018

由于路由通常会把多个组件牵扯到一起操作,所以一般对其的测试都在 端到端/集成 阶段进行,处于测试金字塔的上层。不过,做一些路由的单元测试还是大有益处的。

对于与路由交互的组件,有两种测试方式:

  • 使用一个真正的 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'
}
</script>

<NestedRoute>  同样迷你:

<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  文件并导入定义好的路由,写出来一般是这样的:

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.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 常与各种组件互相影响以达到某些效果,也应该做一些集成测试以确保所有事情如预期般工作。

总结

本文讲述了:

  • 测试由 Vue Router 条件渲染的组件
  • 用 jest.mock  和 localVue  去 mock Vue 组件
  • 从 router 中解耦全局导航 guard 并对其独立测试
  • 用 jest.mock  来 mock 一个模块

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript获取table中某一列的值的方法
May 06 Javascript
js实现的黑背景灰色二级导航菜单效果代码
Aug 24 Javascript
BootStrap Typeahead自动补全插件实例代码
Aug 10 Javascript
jQuery基于排序功能实现上移、下移的方法
Nov 26 Javascript
Vue2.0实现1.0的搜索过滤器功能实例代码
Mar 20 Javascript
node.js express中app.param的用法详解
Jul 16 Javascript
angular2+node.js express打包部署的实战
Jul 27 Javascript
JavaScript代码执行的先后顺序问题
Oct 29 Javascript
解决angular2在双向数据绑定时[(ngModel)]无法使用的问题
Sep 13 Javascript
Vue.js的复用组件开发流程完整记录
Nov 29 Javascript
Vue和React组件之间的传值方式详解
Jan 31 Javascript
Node.js 实现远程桌面监控的方法步骤
Jul 02 Javascript
在element-ui的el-tree组件中用render函数生成el-button的实例代码
Nov 05 #Javascript
微信小程序http连接访问解决方案的示例
Nov 05 #Javascript
vue多级复杂列表展开/折叠及全选/分组全选实现
Nov 05 #Javascript
浅谈Vue数据响应
Nov 05 #Javascript
vue+canvas实现炫酷时钟效果的倒计时插件(已发布到npm的vue2插件,开箱即用)
Nov 05 #Javascript
基于vue2的canvas时钟倒计时组件步骤解析
Nov 05 #Javascript
基于Vue2实现简易的省市区县三级联动组件效果
Nov 05 #Javascript
You might like
生成缩略图
2006/10/09 PHP
PHP中常用数组处理方法实例分析
2008/08/30 PHP
PHP分多步骤填写发布信息的简单方法实例代码
2012/09/23 PHP
php防注入,表单提交值转义的实现详解
2013/06/10 PHP
Zend Framework教程之Resource Autoloading用法实例
2016/03/08 PHP
Yii rules常用规则示例
2016/03/15 PHP
Laravel 5.4向IoC容器中添加自定义类的方法示例
2017/08/15 PHP
PHP 进度条函数的简单实例
2017/09/19 PHP
PHP后台备份MySQL数据库的源码实例
2019/03/18 PHP
php引用和拷贝的区别知识点总结
2019/09/23 PHP
Yii框架 session 数据库存储操作方法示例
2019/11/18 PHP
JS日历 推荐
2006/12/03 Javascript
jQuery UI-Draggable 参数集合
2010/01/10 Javascript
前端开发必须知道的JS之原型和继承
2010/07/06 Javascript
JQuery之拖拽插件实现代码
2011/04/14 Javascript
详解Vue.js——60分钟组件快速入门(上篇)
2016/12/05 Javascript
vue-resourse将json数据输出实例
2017/03/08 Javascript
JavaScript全屏和退出全屏事件总结(附代码)
2017/08/17 Javascript
微信小程序实现tab切换效果
2017/11/21 Javascript
vue.js根据代码运行环境选择baseurl的方法
2018/02/28 Javascript
图文讲解vue的v-if使用方法
2019/02/11 Javascript
聊聊鉴权那些事(推荐)
2019/08/22 Javascript
Vue+Bootstrap收藏(点赞)功能逻辑与具体实现
2020/10/22 Javascript
Vue+Element-U实现分页显示效果
2020/11/15 Javascript
[02:42]岂曰无衣,与子同袍!DOTA2致敬每一位守护人
2020/02/17 DOTA
Python简明入门教程
2015/08/04 Python
Python IDE Pycharm中的快捷键列表用法
2019/08/08 Python
Django Admin中增加导出CSV功能过程解析
2019/09/04 Python
Python抓包并解析json爬虫的完整实例代码
2020/11/03 Python
彻底解决pip下载pytorch慢的问题方法
2021/03/01 Python
一份比较全的PHP面试题
2016/07/29 面试题
旅游节目策划方案
2014/05/26 职场文书
辩论会主持词
2015/07/03 职场文书
教你使用vscode 搭建react-native开发环境
2021/07/07 Javascript
dubbo集成zipkin获取Traceid的实现
2021/07/26 Java/Android
日本动漫十大公认神作:第五现已全网禁播,《死亡笔记》在榜
2022/03/18 日漫