对 Vue-Router 进行单元测试

发表于:2018-11-05 11:26

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:佚名    来源:51testing采编

  由于路由通常会把多个组件牵扯到一起操作,所以一般对其的测试都在 端到端/集成 阶段进行,处于测试金字塔的上层。不过,做一些路由的单元测试还是大有益处的。
  对于与路由交互的组件,有两种测试方式:
  使用一个真正的 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

   上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号