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

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

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

  • 使用一个真正的 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 一个模块

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

(0)

相关推荐

  • vue-router重定向和路由别名的使用讲解

    一.重定向(你访问这个路径,但他跳到另一个路径,地址栏中显示目标路由的那个路径) "重定向"的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b 二.路由别名(就是给这个路由起了个名字,访问这个路由的时候不需要访问path 而是访问alias后面的) /a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样. 上面对应的路由配置为: const router = new VueRouter({

  • Vuex中的State使用介绍

    现在在上一篇为什么要使用Vuex的介绍理解基础上使用Vuex中的State,一个正面例子来证实,同时也介绍一下Vue核心概念State. Vuex 使用单一状态树--是的,用一个对象就包含了全部的应用层级状态.至此它便作为一个"唯一数据源 (SSOT)"而存在.这也意味着,每个应用将仅仅包含一个 store 实例.单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照. 单状态树和模块化并不冲突--在后面的章节里我们会讨论如何将状态和状态变

  • vue-router懒加载速度缓慢问题及解决方法

    懒加载:也叫延迟加载,即在需要的时候进行加载,随用随载. 像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时. 简单的说就是:进入首页不用一次加载过多资源造成用时过长!!! 懒加载的方式: import Vue from 'vue' import

  • 详解vue-router导航守卫

    当做Vue-cli项目的时候需要在路由跳转前做一些验证,比如登录验证,是网站中的普遍需求. 对此,vue-router 提供的 beforeEach可以方便地实现全局导航守卫(navigation-guards).组件内部的导航守卫函数使用相同,只是函数名称不同(beforeRouteEnter .beforeRouteUpdate(2.2 新增) .beforeRouteLeave). 钩子(Hook),早期编程可能有个概念叫句柄,不知道将两者类比而且强行归为一类是不是合适.钩子的用处是在某个

  • vue-router启用history模式下的开发及非根目录部署方法

     为什么要有 hash 和 history 对于 Vue 这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义.前端路由的核心,就在于 -- 改变视图的同时不会向后端发出请求. 为了达到这一目的,浏览器当前提供了以下两种支持: 1.hash -- 即地址栏 URL 中的 # 符号(此 hash 不是密码学里的散列运算). 比如这个 URL:http://www.abc.com/#/hello,hash 的值为 #/hello.它

  • 在移动端使用vue-router和keep-alive的方法示例

    对于web开发和移动端开发,两者在路由上的处理是不同的.对于移动端来说,页面的路由是相当于栈的结构的.vue-router与keep-alive提供的路由体验与移动端是有一定差别的,因此常常开发微信公众号的我想通过一些尝试来将两者的体验拉近一些. 目标 问题 首先一个问题是keep-alive的行为.我们可以通过keep-alive来保存页面状态,但这样的行为对于类似于APP的体验是有些奇怪的.例如我们的应用有首页.列表页.详情页3个页面,当我们从列表页进入详情页再返回,此时列表页应当是keep

  • vue-router beforeEach跳转路由验证用户登录状态

    使用vue判断验证用户登录状态 导航钩子类似于生命周期钩子,包含路由进入前,进入后,更新时,退出前等几个周期,主要用于控制导航的前进后退或跳转等. 其中router.beforeEach就是路由进入前的周期,同时有路由的来源和去向两个参数,可以判断和控制当前路由的走向和重定向. 一般router.beforeEach配合vuex全局状态储存使用,验证用户登录状态.也可以结合sessionStorage 和localStorage使用,原理相同. 用户登录状态验证 路由配置 定义需要判断登录状态的

  • vue-router判断页面未登录自动跳转到登录页的方法示例

    1.定义路由的时候配置meta属性,requireAuth用来标记跳转的这个路由是否需要检测登录 下面的两个页面,登录页不需要检测,首页需要检测 const routers = [ { path: '/', component: App, children: [ { path: '/login', component: Login, meta: { title: '登录' } }, { path: '/home', component: Home, meta: { title: '首页', re

  • vue-router传递参数的几种方式实例详解

    vue-router传递参数分为两大类 编程式的导航 router.push 声明式的导航 <router-link> 编程式的导航 router.push 编程式导航传递参数有两种类型:字符串.对象. 字符串 字符串的方式是直接将路由地址以字符串的方式来跳转,这种方式很简单但是不能传递参数: this.$router.push("home"); 对象 想要传递参数主要就是以对象的方式来写,分为两种方式:命名路由.查询参数,下面分别说明两种方式的用法和注意事项. 命名路由

  • Vue核心概念Getter的使用方法

    有时候我们需要从store中的state中派生出一些状态. 如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它--无论哪种方式都不是很理想. Vuex 允许我们在store中定义getter属性(可以认为是 store 的计算属性).就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算. 比如这里我们在Page4.vue里面需要对商品价格加倍,我们就可以使用Getter. 使用Getter store

随机推荐