对 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 相关文章推荐
json跟xml的对比分析
Jun 10 Javascript
使用Jquery来实现可以输入值的下拉选单 雏型
Dec 06 Javascript
javascript动画算法实例分析
Jul 31 Javascript
jQuery ajax分页插件实例代码
Jan 27 Javascript
JavaScript函数节流概念与用法实例详解
Jun 20 Javascript
Webpack实现按需打包Lodash的几种方法详解
May 08 Javascript
jQuery UI 实例讲解 - 日期选择器(Datepicker)
Sep 18 jQuery
AngularJS实现与后台服务器进行交互的示例讲解
Aug 13 Javascript
小程序云开发实战小结
Oct 25 Javascript
layui: layer.open加载窗体时出现遮罩层的解决方法
Sep 26 Javascript
JavaScript组合模式---引入案例分析
May 23 Javascript
微信小程序之导航滑块视图容器功能的实现代码(简单两步)
Jun 19 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
用PHP查询域名状态whois的类
2006/11/25 PHP
PHP面向对象法则
2012/02/23 PHP
php实现上传图片生成缩略图示例
2014/04/13 PHP
ThinkPHP内置jsonRPC的缺陷分析
2014/12/18 PHP
PHP实现简单ajax Loading加载功能示例
2016/12/28 PHP
css3实现背景模糊的三种方式
2021/03/09 HTML / CSS
非常好的js代码
2006/06/27 Javascript
javascript 正则替换 replace(regExp, function)用法
2010/05/22 Javascript
js获取浏览器的可视区域尺寸的实现代码
2011/11/30 Javascript
JS获取页面窗口大小的代码解读
2011/12/01 Javascript
js函数中onmousedown和onclick的区别和联系探讨
2013/05/19 Javascript
js 获取后台的字段 改变 checkbox的被选中的状态 代码
2013/06/05 Javascript
javaScript实现浮点数转十六进制字符
2013/10/29 Javascript
js支持键盘控制的左右切换立体式图片轮播效果代码分享
2015/08/26 Javascript
利用three.js画一个3D立体的正方体示例代码
2017/11/19 Javascript
Vue实现美团app的影院推荐选座功能【推荐】
2018/08/29 Javascript
jQuery实现获取当前鼠标位置并输出功能示例
2019/01/05 jQuery
使用vuepress搭建静态博客的示例代码
2019/02/14 Javascript
vue中的双向数据绑定原理与常见操作技巧详解
2020/03/16 Javascript
jQuery实现移动端图片上传预览组件的方法分析
2020/05/01 jQuery
[07:06]2018DOTA2国际邀请赛寻真——卫冕冠军Team Liquid
2018/08/10 DOTA
[49:05]OG vs Newbee 2019DOTA2国际邀请赛淘汰赛 胜者组 BO3 第二场 8.21.mp4
2020/07/19 DOTA
python批量下载图片的三种方法
2013/04/22 Python
自己编程中遇到的Python错误和解决方法汇总整理
2015/06/03 Python
Python实现字典按照value进行排序的方法分析
2017/12/23 Python
使用Python监控文件内容变化代码实例
2018/06/04 Python
python 定义n个变量方法 (变量声明自动化)
2018/11/10 Python
python 实现识别图片上的数字
2019/07/30 Python
PyTorch中permute的用法详解
2019/12/30 Python
Python list和str互转的实现示例
2020/11/16 Python
Booking.com英国官网:全球酒店在线预订网站
2018/04/21 全球购物
希腊品牌鞋类销售网站:epapoutsia.gr
2020/03/18 全球购物
几道Java和数据库的面试题
2013/05/30 面试题
创先争优承诺书
2015/01/20 职场文书
2016十一国庆节慰问信
2015/12/01 职场文书
2016年中秋节寄语大全
2015/12/07 职场文书