为什么要 Mock Vue Router?
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,断言时却显示没调用」,大概率是这几个原因:
- Mock 时机不对:Vue3 项目里,mount 组件时没把带 mock 路由的 app 实例传进去,要检查
mount选项里的global.app是否正确。 - Mock 对象结构错了:比如把
$router.push写成对象而不是函数,或者$route少了params这类关键属性。 - 测试用例污染:多个用例共享同一个 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 createRouter 和 createWebHistory:
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前端网发表,如需转载,请注明页面地址。
code前端网



