一、测试前,先把环境搭对
很多做Vue项目开发的同学,在写单元测试时碰到Vue Router相关逻辑就犯难——路由组件能不能正确渲染?导航守卫里的权限判断逻辑对不对?动态路由参数传没传对?编程式导航有没有触发?别慌,这篇文章用问答思路,把vue router结合jest测试的关键场景和实操方法拆明白,跟着步骤走就能学会~
要测Vue Router,得先让测试环境里的Vue实例能正常“认”路由,Jest本身是JS测试框架,得结合Vue Test Utils(组件测试工具库)和Vue Router的测试友好配置。项目里得有这些依赖:@vue/test-utils
(测Vue组件)、vue-router
(路由核心)、jest
(测试框架),还有处理单文件组件的@vue/vue3-jest
(Vue3项目)或vue-jest
(Vue2)。
测试时要创建仅用于测试的路由实例,因为真实项目里用的是createWebHistory
(浏览器历史),测试环境用createMemoryHistory
更合适——它不操作真实浏览器历史,性能好还没副作用,举个创建测试路由的例子:
// 假设测试文件是example.spec.js import { createRouter, createMemoryHistory } from 'vue-router' import { createApp } from 'vue' import { mount } from '@vue/test-utils' import Home from '@/components/Home.vue' // 要测试的路由组件 // 定义测试用的路由规则 const testRoutes = [ { path: '/', component: Home }, { path: '/about', component: () => import('@/components/About.vue') } // 异步组件也能测 ] // 创建测试路由实例 const testRouter = createRouter({ history: createMemoryHistory(), // 内存历史,无浏览器依赖 routes: testRoutes }) // 创建Vue应用并注入路由 const app = createApp({}) app.use(testRouter)
为啥这么做?因为Vue组件里的$router
和$route
是靠app.use(router)
注入的,测试时得模拟这个过程,不然组件里拿不到路由实例,测试肯定报错。
路由组件渲染,测的是“路径对应组件对不对”
最基础的需求:访问时,Home组件有没有渲染?这一步要解决“路由匹配”和“组件渲染”的联动测试。
实操步骤:
- 先把路由跳转到目标路径(;
- 等路由“就绪”(因为路由跳转是异步的,得用
await router.isReady()
等它完成); - 用
mount
渲染组件,同时注入测试路由; - 断言组件是否存在、内容是否符合预期。
看例子:
test('访问根路由时,Home组件正确渲染', async () => { // 步骤1:跳转到根路由 await testRouter.push('/') // 步骤2:等路由准备好(异步操作完成) await testRouter.isReady() // 步骤3:渲染Home组件,注入测试路由 const wrapper = mount(Home, { global: { plugins: [testRouter] // 把测试路由注入到全局,组件里才能拿到$router } }) // 步骤4:断言 expect(wrapper.exists()).toBe(true) // 组件确实渲染了 expect(wrapper.find('h1').text()).toBe('首页') // 假设Home里有h1写着“首页” })
要是用shallowMount
(只渲染当前组件,不渲染子组件),适合测组件结构但不需要子组件渲染的场景;如果要测子组件是否加载,就用mount
。
导航守卫测试:权限控制逻辑对不对?
导航守卫(比如beforeEach
全局守卫、beforeEnter
路由独享守卫)是路由权限控制的核心,举个常见场景:访问需要登录的页面(比如/profile
),如果没登录,自动跳转到/login
。
先写守卫逻辑(示例):
// 假设在router/index.js里 import { createRouter, createMemoryHistory } from 'vue-router' import Profile from '@/components/Profile.vue' import Login from '@/components/Login.vue' // 模拟登录状态工具函数(真实项目里可能是store或cookie判断) export function isLoggedIn() { return localStorage.getItem('token') ? true : false } const routes = [ { path: '/profile', component: Profile, meta: { requiresAuth: true } // 标记需要登录 }, { path: '/login', component: Login } ] const router = createRouter({ history: createMemoryHistory(), routes }) // 全局前置守卫:判断权限 router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !isLoggedIn()) { next('/login') // 没登录,跳转到登录页 } else { next() // 正常放行 } }) export default router
测试守卫逻辑(未登录时跳转到登录页):
要模拟“未登录”状态,所以得mockisLoggedIn
函数返回false
,Jest的jest.mock
能帮我们拦截模块导出,替换成模拟函数。
test('未登录时访问/profile,自动跳转到/login', async () => { // 步骤1:mock isLoggedIn,让它返回false(模拟未登录) jest.mock('@/router', () => ({ ...jest.requireActual('@/router'), // 保留原模块其他导出 isLoggedIn: () => false })) // 步骤2:跳转到/profile await testRouter.push('/profile') await testRouter.isReady() // 等路由跳转完成 // 步骤3:断言当前路由是否是/login expect(testRouter.currentRoute.value.path).toBe('/login') })
要是测试组件内守卫(比如beforeRouteEnter
),因为它在组件实例创建前触发,测试时要注意异步处理,比如组件里用beforeRouteEnter
做数据预加载:
<template> <div>{{ userInfo.name }}</div> </template> <script setup> import { onBeforeRouteEnter } from 'vue-router' import { ref } from 'vue' const userInfo = ref({}) onBeforeRouteEnter((to, from, next) => { // 模拟请求用户信息(真实项目里是API请求) const mockUser = { name: '小明' } next(vm => { vm.userInfo = mockUser // 把数据传给组件实例 }) }) </script>
测试这个守卫时,要确保next
里的回调执行,组件数据更新:
test('beforeRouteEnter触发数据预加载', async () => { await testRouter.push('/user') await testRouter.isReady() const wrapper = mount(User, { global: { plugins: [testRouter] } }) // 断言数据是否加载成功 expect(wrapper.text()).toContain('小明') })
动态路由测试:参数传没传对?
动态路由(比如/user/:id
)的核心是参数匹配和组件响应参数变化,测试要覆盖“参数显示是否正确”和“同一路由参数变化时组件是否更新”。
场景1:访问/user/123
,组件显示ID为123
路由配置:
{ path: '/user/:id', component: User }
User组件:
<template> <div>用户ID:{{ $route.params.id }}</div> </template>
测试用例:
test('动态路由参数正确渲染', async () => { // 跳转到带参数的路由 await testRouter.push('/user/123') await testRouter.isReady() const wrapper = mount(User, { global: { plugins: [testRouter] } }) // 断言参数显示 expect(wrapper.text()).toContain('用户ID:123') // 也可以直接断言$route.params expect(wrapper.vm.$route.params.id).toBe('123') })
场景2:同一路由,参数变化时组件更新
比如从/user/123
跳转到/user/456
,User组件要显示456,这时候要测组件的响应性。
测试用例:
test('动态路由参数变化时组件更新', async () => { // 第一次跳转 await testRouter.push('/user/123') await testRouter.isReady() const wrapper = mount(User, { global: { plugins: [testRouter] } }) expect(wrapper.text()).toContain('123') // 第二次跳转(同一路由,参数变化) await testRouter.push('/user/456') await testRouter.isReady() // 断言组件内容更新 expect(wrapper.text()).toContain('456') })
编程式导航测试:按钮点击后路由跳了没?
组件里常用this.$router.push
或useRouter().push
做“点击按钮跳转”,测试要验证点击事件是否触发路由方法,以及最终路由是否变化。
组件示例(ButtonLink.vue):
<template> <button @click="goToAbout">去关于页</button> </template> <script setup> import { useRouter } from 'vue-router' const router = useRouter() const goToAbout = () => { router.push('/about') } </script>
测试用例(两种思路):
思路1:测最终路由是否变化
test('点击按钮后路由跳转到/about(测结果)', async () => { const wrapper = mount(ButtonLink, { global: { plugins: [testRouter] } }) // 触发点击事件 await wrapper.find('button').trigger('click') await testRouter.isReady() // 等路由跳转完成 // 断言当前路由 expect(testRouter.currentRoute.value.path).toBe('/about') })
思路2:测router.push是否被调用(更直接)
test('点击按钮后router.push被调用(测方法)', async () => { const wrapper = mount(ButtonLink, { global: { plugins: [testRouter] } }) // 用jest.spyOn监测router.push方法 const pushSpy = jest.spyOn(testRouter, 'push') await wrapper.find('button').trigger('click') // 断言push被调用,且参数是'/about' expect(pushSpy).toHaveBeenCalledWith('/about') })
两种方法各有用途:如果导航守卫有拦截(比如跳转到/about前被守卫拦下来改跳/login),思路1测的是“最终结果”,思路2测的是“组件是否触发了导航”,根据需求选就行~
路由元信息(meta)测试:页面标题设对了没?
路由元信息(比如meta.title
)常用在设置页面标题、权限标记等场景,测试要验证组件是否正确读取meta信息。
场景:访问/about
时,文档标题设为“关于我们”
路由配置:
{ path: '/about', component: About, meta: { title: '关于我们' } }
About组件(设置页面标题):
<template> <div>关于页内容</div> </template> <script setup> import { onMounted } from 'vue' import { useRoute } from 'vue-router' onMounted(() => { const route = useRoute() document.title = route.meta.title }) </script>
测试用例:
test('路由meta设置页面标题', async () => { await testRouter.push('/about') await testRouter.isReady() const wrapper = mount(About, { global: { plugins: [testRouter] } }) // 断言文档标题是否正确 expect(document.title).toBe('关于我们') })
这里要注意:Jest运行在JSOM环境里,document
对象是模拟的,所以能直接断言,如果项目里对document
有特殊处理(比如用自定义渲染器),可能需要额外mock,但一般情况不用操心~
避坑指南:这些细节容易栽跟头
测试Vue Router时,有些“隐性问题”不注意就会导致测试失败,提前避坑能省很多时间:
异步路由(import())的测试
如果路由组件是异步加载的(比如component: () => import('@/components/Async.vue')
),测试时要提前加载组件,否则Jest可能找不到组件导致报错,可以用beforeAll
提前加载:
beforeAll(async () => { await import('@/components/Async.vue') // 提前加载异步组件 })
路由异步操作一定要等!
路由的push
、isReady
都是异步函数,测试时必须用async/await
包裹,比如下面的错误写法(没等路由就绪):
// 错误示例!没等router.isReady(),断言时路由还没更新 test('错误示范', () => { testRouter.push('/') const wrapper = mount(Home, { ... }) expect(wrapper.text()).toContain('首页') // 可能失败,因为路由还没匹配完成 })
正确写法要加await
:
test('正确示范', async () => { await testRouter.push('/') await testRouter.isReady() const wrapper = mount(Home, { ... }) expect(...) // 这时候路由已经就绪 })
多个测试用例要“路由隔离”
如果多个测试用例共用同一个路由实例,前一个用例的路由状态会影响后一个,解决方法:每个用例前创建新的路由实例,用beforeEach
重置:
let testRouter // 声明路由变量 beforeEach(() => { // 每次测试前创建新的路由实例 testRouter = createRouter({ history: createMemoryHistory(), routes: [...] }) }) test('用例1', async () => { ... }) test('用例2', async () => { ... }) // 每个用例的路由都是新的,互不影响
避免真实API请求
如果路由守卫或组件里有API请求(比如登录验证、数据加载),测试时要mock API,否则测试会变慢还可能失败,用jest.mock
拦截API模块,返回模拟数据:
// 假设auth.js里有loginApi函数 jest.mock('@/api/auth', () => ({ loginApi: () => Promise.resolve({ token: 'mock_token' }) }))
vue router结合jest测试的核心是“模拟路由环境 + 覆盖关键场景”:从组件渲染到守卫逻辑,从动态参数到编程式导航,每个场景都要结合异步处理、mock技术和断言逻辑,只要把环境搭对、异步等够、mock用对,路由测试其实没那么难~现在可以动手把项目里的路由逻辑挨个测一遍,保证代码质量更稳当~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。