Code前端首页关于Code前端联系我们

为什么要 Mock Vue Router?

terry 7小时前 阅读数 12 #Vue
文章标签 Vue Router;Mock

p>在 Vue 项目做单元测试时,碰到路由相关逻辑总有点头疼?比如组件里用了 $router.push、依赖 $route.params,或者要测试导航守卫…这时候用 Jest mock Vue Router 就能解决依赖隔离和行为断言的问题,下面从「为什么要 mock」「怎么 mock 基础场景」到「复杂场景处理」一步步讲清楚~

先想明白测试的「隔离性」——单元测试要聚焦被测逻辑,少依赖外部环境,真实 Vue Router 有这些麻烦:

  • 环境依赖:createWebHistory 依赖浏览器的 history API,测试环境(Node.js)里跑会报错;
  • 副作用干扰:路由跳转真的会改变页面状态,测试用例之间容易互相影响;
  • 逻辑不可控:比如要测试「点击按钮跳转到 /about」,总不能真的让路由跳转后再断言吧?mock 能让路由方法变成可监听的「桩函数」,直接看有没有被调用、传啥参数。

简单说,mock 后能把路由变成「自己人」,想让它咋表现就咋表现,测试逻辑更干净稳定。

基础场景:组件里用 $router/$route 咋 Mock?

不管是 Vue2 还是 Vue3,核心思路是「把组件里依赖的 $router/$route 换成 mock 对象」,分场景看:

Vue2 项目(选项式 API 为主)

组件里通过 this.$router.push 跳转?测试时给 Vue 原型打补丁就行,举个例子,有个按钮点击后跳转到 /detail

<!-- MyButton.vue -->
<template><button @click="$router.push('/detail')">跳转</button></template>
<script>
export default { name: 'MyButton' }
</script>

测试文件里这么写:

import Vue from 'vue'
import { shallowMount } from '@vue/test-utils'
import MyButton from '@/components/MyButton.vue'
// 先 mock 掉 vue-router 模块(可选,若不需要真实模块逻辑)
jest.mock('vue-router')
describe('MyButton 路由跳转测试', () => {
  beforeEach(() => {
    // 每次测试前重置 $router,避免用例互相影响
    Vue.prototype.$router = {
      push: jest.fn() // 把 push 换成 jest 函数,方便断言
    }
  })
  it('点击按钮触发 $router.push', () => {
    const wrapper = shallowMount(MyButton)
    wrapper.find('button').trigger('click')
    // 断言 push 被调用,且参数是 /detail
    expect(Vue.prototype.$router.push).toHaveBeenCalledWith('/detail')
  })
})

原理:Vue2 里路由实例是挂在 Vue.prototype 上的,所以给原型的 $router 赋值 mock 对象,组件里的 this.$router 就会指向它。

Vue3 项目(组合式 API 为主)

Vue3 用 app.config.globalProperties 挂载全局属性,测试时要把 mock 的路由挂到 app 实例上,比如组件用组合式 API,通过 useRouter() 拿路由:

<!-- MyButton.vue -->
<template><button @click="handleClick">跳转</button></template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const handleClick = () => router.push('/detail')
</script>

测试文件这么写:

import { createApp } from 'vue'
import { mount } from '@vue/test-utils'
import MyButton from '@/components/MyButton.vue'
jest.mock('vue-router') // mock 整个 vue-router 模块
describe('MyButton 组合式 API 路由测试', () => {
  let app // 保存 app 实例,方便复用
  beforeEach(() => {
    app = createApp({})
    // 给全局属性注入 mock 的 $router
    app.config.globalProperties.$router = {
      push: jest.fn()
    }
  })
  it('点击按钮触发 router.push', () => {
    const wrapper = mount(MyButton, {
      global: { app } // 挂载时传入 app 实例
    })
    wrapper.find('button').trigger('click')
    // 断言 push 被调用
    expect(app.config.globalProperties.$router.push).toHaveBeenCalledWith('/detail')
  })
})

要是组件里用 useRouter(),还得 mock useRouter 方法返回 mock 对象:

jest.mock('vue-router', () => {
  const original = jest.requireActual('vue-router')
  return {
    ...original,
    useRouter: jest.fn(() => ({
      push: jest.fn()
    }))
  }
})
// 测试用例里直接断言 useRouter 返回的 router.push
import { useRouter } from 'vue-router'
it('useRouter 触发跳转', () => {
  const router = useRouter()
  // ...触发组件逻辑后...
  expect(router.push).toHaveBeenCalled()
})

导航守卫咋 Mock?

导航守卫分「全局守卫(如 router.beforeEach)」和「组件内守卫(如 beforeRouteEnter)」,mock 思路是「模拟守卫的参数,手动触发守卫函数」。

全局前置守卫(router.beforeEach)测试

假设路由配置里有个全局守卫,判断用户是否登录,没登录就跳转到 /login:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})
router.beforeEach((to, from, next) => {
  if (!isLogin() && to.path !== '/login') {
    next('/login')
  } else {
    next()
  }
})
export default router

测试这个守卫,要 mock to from next,然后调用守卫函数:

import router from '@/router'
jest.mock('vue-router', () => {
  const original = jest.requireActual('vue-router')
  return {
    ...original,
    createRouter: jest.fn(() => ({
      beforeEach: jest.fn(), // 让 router.beforeEach 能被 mock
      // 其他路由方法按需 mock
    })),
    createWebHistory: jest.fn()
  }
})
describe('全局前置守卫测试', () => {
  it('未登录访问 /home 跳转到 /login', () => {
    // mock isLogin 始终返回 false(模拟未登录)
    jest.spyOn(utils, 'isLogin').mockReturnValue(false)
    // 模拟 to 和 from 对象
    const to = { path: '/home' }
    const from = { path: '/' }
    const next = jest.fn() // mock next 函数
    // 调用守卫(router.beforeEach 里注册的回调,取第一个执行)
    router.beforeEach.mock.calls[0][0](to, from, next)
    // 断言 next 被调用时参数是 /login
    expect(next).toHaveBeenCalledWith('/login')
  })
})

组件内守卫(beforeRouteEnter)测试

组件里用 beforeRouteEnter 做进入前逻辑,比如预加载数据:

<!-- User.vue -->
<template><div>{{ userInfo.name }}</div></template>
<script>
export default {
  data() { return { userInfo: {} } },
  beforeRouteEnter(to, from, next) {
    fetchUser(to.params.id).then(res => {
      next(vm => {
        vm.userInfo = res.data
      })
    })
  }
}
</script>

测试时要模拟 to from next,还要处理 next 里的回调(给组件实例赋值):

import { mount } from '@vue/test-utils'
import User from '@/components/User.vue'
jest.mock('@/api', () => ({
  fetchUser: jest.fn(() => Promise.resolve({ data: { name: '张三' } }))
})) // mock 接口请求
describe('组件内守卫 beforeRouteEnter 测试', () => {
  it('进入页面时预加载用户数据', async () => {
    const to = { params: { id: '123' } }
    const from = { path: '/' }
    const next = jest.fn()
    // 调用组件的 beforeRouteEnter 守卫
    User.beforeRouteEnter(to, from, next)
    await Promise.resolve() // 等待 fetchUser 异步完成
    // 模拟组件实例 vm,执行 next 的回调
    const wrapper = mount(User)
    next(wrapper.vm) // 把 wrapper.vm 传给 next 的回调
    // 断言数据已加载
    expect(wrapper.text()).toContain('张三')
    expect(next).toHaveBeenCalled()
  })
})

动态路由参数咋 Mock?

组件依赖 $route.params(比如用户 ID),测试不同参数下的渲染逻辑,关键是「灵活设置 $route 的 params 属性」。

比如组件根据 $route.params.id 渲染用户信息:

<template><div>{{ userId }}</div></template>
<script>
export default {
  computed: {
    userId() {
      return this.$route.params.id
    }
  }
}
</script>

测试不同 id 的情况:

import Vue from 'vue'
import { shallowMount } from '@vue/test-utils'
import UserId from '@/components/UserId.vue'
describe('动态路由参数测试', () => {
  beforeEach(() => {
    // 每次测试前重置 $route,避免污染
    Vue.prototype.$route = { params: {} }
  })
  it('参数 id=1 时渲染 1', () => {
    Vue.prototype.$route.params.id = '1'
    const wrapper = shallowMount(UserId)
    expect(wrapper.text()).toContain('1')
  })
  it('参数 id=2 时渲染 2', () => {
    Vue.prototype.$route.params.id = '2'
    const wrapper = shallowMount(UserId)
    expect(wrapper.text()).toContain('2')
  })
})

要是组件用 watch $route 响应参数变化(比如选项式 API 的 watch),测试时改完 $route 后要触发更新:

<template><div>{{ userId }}</div></template>
<script>
export default {
  data() { return { userId: '' } },
  watch: {
    '$route.params.id'(newId) {
      this.userId = newId
    }
  }
}
</script>

测试代码:

it('watch 路由参数变化更新数据', () => {
  Vue.prototype.$route = { params: { id: '1' } }
  const wrapper = shallowMount(UserId)
  expect(wrapper.vm.userId).toBe('1')
  // 改变 $route.params.id
  Vue.prototype.$route.params.id = '2'
  wrapper.vm.$forceUpdate() // 触发 watch 回调(Vue2 需手动 forceUpdate)
  expect(wrapper.vm.userId).toBe('2')
})

常见坑:Mock 后路由方法没生效?

碰到「明明 mock 了 $router.push,断言时却显示没调用」,大概率是这几个原因:

  1. Mock 时机不对:Vue3 项目里,mount 组件时没把带 mock 路由的 app 实例传进去,要检查 mount 选项里的 global.app 是否正确。
  2. Mock 对象结构错了:比如把 $router.push 写成对象而不是函数,或者 $route 少了 params 这类关键属性。
  3. 测试用例污染:多个用例共享同一个 mock 对象,前一个用例的调用痕迹影响了后一个,解决方法是在 beforeEach 里重置 mock 对象。

进阶:Vue Router 4 + 组合式 API 咋 Mock?

Vue Router 4 用 createRouter createWebHistory 初始化路由,测试时要 mock 这些工厂函数,返回假的路由实例。

比如项目里路由配置:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [...]
const router = createRouter({
  history: createWebHistory(),
  routes
})
export default router

测试全局守卫前,mock createRoutercreateWebHistory

jest.mock('vue-router', () => {
  const original = jest.requireActual('vue-router')
  return {
    ...original,
    createRouter: jest.fn(() => ({
      beforeEach: jest.fn(),
      push: jest.fn(),
      // 其他需要的路由方法
    })),
    createWebHistory: jest.fn(() => ({})), // .mockResolvedValue 之类的按需写
    useRouter: jest.fn(() => ({
      push: jest.fn()
    }))
  }
})
// 测试用例里就能自由控制路由行为
import router from '@/router'
it('测试 createRouter 返回的实例', () => {
  expect(createRouter).toHaveBeenCalled()
  router.beforeEach((to, from, next) => { ... }) // 调用守卫逻辑
})

Jest mock Vue Router 核心是「用假对象替代真实路由,把路由方法变成可断言的桩函数」,不管是基础的 $router/$route 模拟,还是导航守卫、动态路由这些复杂场景,只要记住「隔离依赖 + 控制输入输出」,测试逻辑就会清晰又稳定~要是你在测试时碰到其他路由相关问题,评论区聊聊?

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门