测试VueRouter

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26

了解 VueRouter

了解路由

服务端路由:从服务器请求并渲染页面,匹配路径(/)来响应相应的路由。弊端是浏览器每次加载新文档(HTML)时,当前应用程序的状态值就会丢失,需要使用 cookie、本地存储以及URL里的查询参数来保存页面间的状态值,使开发变得复杂。

客户端路由:页面内容直接在客户端渲染,无须向服务端发新请求。当页面点击链接跳转页面的时候,客户端路由将阻止浏览器向服务器发送请求,作为替代,客户端路由会更改URL而不会导致页面的重新加载,并且页面将会使用对应的内容进行渲染,这里的关键点就是页面内容更新,但页面仍保留了与之前相同的 state。

了解VueRouter的概念

VueRouter 是 Vue 客户端路由库,VueRouter 将 URL 的路径与其应渲染的组件配对,你可以使用一个路由数组来配置 VueRouter 要匹配的路径,一个路由配置对象有多个属性,其中最重要的是 path 和 component 属性,path 属性用来与 URL 的当前路径进行匹配,而 component 属性就是路径匹配后应该渲染的组件。

默认情况下, VueRouter 每次只会匹配一个路由,你需要渲染一个 RouterView 组件 (VueRouter 的内置组件)来渲染当前路由匹配到的组件,你可以将其理解为占位符,如果当前的路由是 /about 并且匹配到了对应的 About 组件,那么 RouterView 组件将渲染为 About 组件。

<!-- App.vue -->
<template>
  <router-view />
</template>
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import About from './components/About.vue';
import router from './router';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    component: App
  },
  {
    path: '/about',
    component: About
  }
]

new Vue({
  router,
  el: '#app', // 对应 index.html模板
  render: h => h(App)
})

VueRouter 可以借助 RouterLink 在页面进行跳转,RouterLink 默认使用 <a> 元素进行渲染,当点击该元素的时候, VueRouter会阻止浏览器重新加载页面,并更新 URL,重新渲染 RouterView。

<router-link to="/about" /> 关于我们 <router-link>

了解动态路由匹配

动态路由匹配通常会使用路由的一部分来生成页面的内容,比如路径 /product/1,这里路径的第一段就是产品的ID,在渲染的时候就可以根据对应的ID生成对应的内容。

你可以在路径中定义动态路径参数,并使用 : 进行标示。

const routes = [
  {
    path: '/product/:id',
    component: ProductShow,
  },
  {
    path: '/:type(top|new)', // 匹配 /top 或 /new,可以使用 $router.params.type 访问到
    component: news,
  },
  {
    path: '/:type(top|new)/:page',
    // 可以匹配 /top/1 或 /new/20
    // 使用 $router.params.type 访问到 top 还是 new
    // 使用 $router.params.page  匹配到 1 或 20
    component: moreNews,
  },
  {
    path: '/',
    redirect: '/top' // 根路径直接重定向到/top,使用 redirect 则不需要使用component属性
  }
]

在实例中可以获取匹配到的动态参数路径 $route.params.id,如果想要达到精准的匹配效果,你可以在动态路径中使用正则表达式。

VueRouter 的配置对象包含两个属性: mode 和 routes,mode 用来设置 VueRouter 以哪种方式控制 URL,history 模式会告诉 VueRouter 使用底层的 window.history.pushState 方法来设置 URL,而不触发页面的重新加载。 routes 是一个路由数组,它定义了哪些路由应该被匹配到。

测试VueRouter

测试路由属性

当 VueRouter 被安装到 Vue 上时,它会添加两个实例属性:$route$router$route 属性包含了有关当前匹配路由的所有信息,其中包括路由参数中的任何动态字段。$router 是你在入口文件传递给 Vue 的路由实例,包含了可以控制 VueRouter 的方法,比如更改路由完成跳转。

1、测试 $route 属性

test('route', () => {
  const wrapper = shallowMount(TestComponent, {
    mocks: {
      $route: {
        params: {
          id: 123
        }
      }
    }
  })

  expect(wrapper.text()).toContain('123');
  // 断言 TestComponent 能够根据 $route 的属性,正确渲染传入的id:123
})

更复杂一点的例子:

test('', () => {
  expert.assertions(1);
  const store = createStore(); // 参考上一章
  store.dispatch = jest.fn(() => Promise.resolve());

  const type = 'top';
  const mocks = {
    $route: {
      params: { type }
    }
  }

  createWrapper({ store, mocks }); // 参考上一章
  await flushPromises();
  expect(store.dispath).toHaveBeenCalledWith('fetchListData', { type });
})

// 这就要求组件实现是

this.$store.dispatch('fetchListData' {
  type: this.$route.params.type
})

总结,如果你正在测试的组件需要访问 $route 和 $router 上的属性和方法,而不需要在测试中使它们的值受控,那么可以使用 localVue 安装 VueRouter,因为使用 Vue 实例安装 VueRouter 的时候会把 $route 和 $router 的值设置为只读,因此你就无法在测试中使它们的值受控。

如果你要控制 $route 和 $router 对象中包含的数据,你需要使用 mocks 挂载选项,mocks 挂载选项使得属性在每个挂载组件内都可用。

2、测试 $router 属性

上面的例子,所有传递给路由的参数都是我们模拟的有效值,但是因为用户可以通过更改 URL 来控制页面参数,就可能访问到了未存在的路由,你应该添加一些代码来处理无效值并重定向到有效页。也可能传递不符合规范的参数,比如page/id之类的传值一定要准确,所以要掌握如何测试 $router 属性。

由于 $router 是一个实例属性,因此你可以使用 Vue Test Utils 的 mocks 挂载项在测试中控制 $router 的值。

test('当页码参数超出了最大值重定向到第一页', () => {
  expert.assertions(1);
  const store = createStore({
    getters: {
      maxPage: () => 5
    }
  })

  const mocks = {
    $route: {
      params: { page: 1000 }
    },
    $router: {
      replace: jest.fn(),
    }
  }

  createWrapper({ mocks, store });
  await flushPromises();
  expect(mocks.$router.replace).toHaveBeenCalledWith('/product/1'); // 断言会被重定向到第一页
})

// 所以代码应有这样一段逻辑
if (this.$route.params.page > this.$router.getters.maxPage) {
  this.$router.replace('/product/1');
}

与 Vuex 一样,VueRouter 作为插件,应该避免直接在 Vue 基础构造函数上安装,应该使用 localVue 构造函数。

测试RouterLink组件

要测试链接到其他页的 RouterLink 组件,你需要断言 RouterLink 组件接收到了正确的 to prop,这样它才能在点击后正常的跳转,问题是,VueRouter 并没有到处 RouterLink 和 RouterView 组件,因此无法把 RounterLink 组件当做选择器 wrapper.findComponent(Component) 这样调用。

解决方法是控制渲染成 RouterLink 的组件,把这个受控组件用作选择器,你可以使用 Vue Test Utils 控制已渲染组件,当 Vue 父组件渲染子组件时, Vue 会试图在父组件实例上对子组件进行解析,你可以使用 Vue Test Utils 的 stubs 选项覆盖这个过程。

const wrapper = shallowMount(TestComponent, {
  stubs: {
    RouterLink: 'div',
  }
})

// 这里的意思 设置所有的 RouterLink 组件以 <div> 元素渲染
// RouterLink 属性名还可以写为 router-link \ routerLink

Vue Test Utils 可以输出一个表现就像 RouterLink 的 RouterLinkStub 组件,你可以存根所有的 RouterLink 组件,把它们解析为 RouterLinkStub 组件,使用 RouterLinkStub 作为选择器。

import { shallowMount, RouterLinkStub } from '@vue/test-utils';

test('renders RouterLink', () => {
  const store = createStore({
    getters: {
      maxPage: () => 3,
    }
  })

  const mocks = {
    $route: {
      params: { page: '1' }
    }
  }
  const wrapper = createWrapper({
    store,
    mocks,
    stubs: {
      RouterLink: RouterLinkStub,
    }
  })

  expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/product/1');
  expect(wrapper.findComponent(RouterLinkStub).text()).toBe('下一页');
  // 最大页数是3页,当前在第1页,所以可以正常渲染出下一页的 RouterLink
})

// 测试 router-link 是否渲染

test('测试当没有正确传参的时候,RouterLink不会被渲染,只渲染一个a标签', () => {
  const wrapper = createWrapper(); // 没有传参 $route.params.page
  expect(wrapper.find('a').attributes().href).toBe(undefined);
  expect(wrapper.find('a').text()).toBe('上一页');
})

对应的 RouterLink 的 template 大概如下:

<template>
  <div>
    <router-link
      v-if="$route.params.page > 1"
      :to="'/' + $route.params.type + '/' + ($route.params.page - 1) "
    >
      上一页
    </router-link>
    <!-- 如果上一页不存在 渲染一个a标签 没有herf 即不可点击跳转 -->
    <a v-else> 上一页 </a> 

    <span> {{ $route.params.page || 1 }} / {{ $store.getters.maxPage }} </span>

    <router-link
      v-if="($route.params.page || 1) < $store.getters.maxPage "
      :to="'/' + $route.params.type + '/' + ($route.params.page + 1) "
    >
      下一页
    </router-link>
    <a v-else> 下一页 </a>
  </div>
</template>

当你安装 VueRouter 时,VueRouter 组件会注册为全局组件,只有在 Vue 实例被实例化之前,VueRouter 被安装到 Vue 构造函数上时(调用了Vue.use(VueRouter)),组件才能渲染它们。

如果你挂载了一个包含了 RouterLink 或 RouterView 的自定义组件,而没有对它们(RouterLink/RouterView)进行存根或者没有在 localVue 上安装 VueRouter,测试输出中会报告出警告信息,如下示例:

[Vue warn]: Unknown custom element: <router-link> - did you register the component correctly? For recursive components, make sure to provide the “name” option.

推荐在之前一直提到的 createWrapper 创建包装器的工厂函数里使用 stubs 挂载选项来存根这些组件,而不是在 localVue 构造函数中安装 VueRouter,以便在需要时对 VueRouter 属性进行覆盖。

Vuex 和 VueRouter 的配合使用

在 Vuex store中使用 VueRouter 的属性可能会很使用,你可以使用 vuex-router-sync 库同步 Vuex 和 VueRouter, 使 route 对象在 store 也可以被获取到。

import { sync } from 'vuex-router-sync'; // npm i -S vuex-router-sync

sync(store, route);

现在 store 中会包含一个 route 对象, 该对象与组件实例中的 $route 值相同。例如,可以使用 $store.route.params.page 访问对应的 route 传参。

所以在测试中可以这样模拟使用:

test('vuex router sync', () => {
  const items = Array(40).fill().map((v, i) => i);
  const store = {
    items,
    route: {
      params: {
        page: '2'
      }
    }
  }

  const relust = getters.displayItems(store); // getters 接收 store 作为参数
  const expectdResult = items.slice(20, 40);
  expect(result).toEqual(expectdResult);
})


// 应该有对应的 getters 如下
const store = {
  // ... 
  getters: {
    displayItems() {
      const page = Number(state.route.params.page) || 1;
      const start = (page - 1) * 20;
      const end = page * 20;
      return state.items.slice(start, end);
    }
  }
}

// 需要注意的是,需要在 createStore 的默认选项中这样设置
// 因为关于 displayItem getters 的测试中没有 router对象 
// 导致访问 state.route 为 undefined 从而出错的的情况

const state = {
  // ...
  route: {
    params: {}
  }
}