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

一、怎么在 Vitest 里搭好 Vue Router 的测试环境?

terry 5小时前 阅读数 11 #Vue
文章标签 Vitest;Vue Router

p>前端项目里,路由逻辑的可靠性直接影响用户体验,Vue Router 作为 Vue 生态的核心路由工具,测试环节可不能马虎,Vitest 凭借快速执行、对 Vue 生态的友好支持,成了很多团队测路由逻辑的首选,但实际写测试时,不少同学会卡在环境配置、跳转逻辑验证、守卫测试这些环节,今天就用问答形式,把 Vue Router + Vitest 测试里的关键问题掰碎了讲~
测试路由前,得先让 Vitest 能识别 Vue Router 的实例,还要模拟浏览器的路由环境(history 模式),以 Vue 3 + Vue Router 4 的项目为例,核心是初始化测试用的 Vue 应用和 Router:

  1. 引入工具与创建 Router:从 vue-router 中导入 createRoutercreateMemoryHistory,结合 @vue/test-utils 的渲染方法,用 createMemoryHistory(适合测试,不操作真实浏览器历史)创建路由实例,并配置测试用的路由表:

    import { createRouter, createMemoryHistory } from 'vue-router'
    import Home from './views/Home.vue'
    import About from './views/About.vue'
    const router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: '/', name: 'Home', component: Home },
        { path: '/about', name: 'About', component: About },
      ]
    })
  2. 挂载 Router 到 Vue 应用:测试用例中,需通过 app.use(router) 将路由注入 Vue 实例,若用 @vue/test-utilsmount 渲染组件,要传入 app 选项保证路由生效:

    import { mount, createApp } from '@vue/test-utils'
    test('测试首页路由渲染', async () => {
      const app = createApp({})
      app.use(router) // 注入路由实例
      const wrapper = mount(Home, { app }) // 组件内可正常使用 useRouter 等 API
      expect(wrapper.text()).toContain('首页内容')
    })
  3. 环境隔离与复用:若多个测试用例共用路由,可通过 beforeEach 重置 Router 实例,避免状态污染:

    let router
    beforeEach(() => {
      router = createRouter({
        history: createMemoryHistory(),
        routes: [/* 每次重新配置路由表 */]
      })
    })

若组件内用了 useRouteruseRoute 等组合式 API,必须确保测试环境正确挂载 Router,否则会报 “No router instance found” 错误。

路由跳转逻辑怎么用 Vitest 验证?

路由跳转分主动跳转(如按钮触发 router.push)、被动跳转(导航守卫触发),测试需覆盖场景并验证“跳转动作”与“页面响应”的一致性。

组件内主动跳转测试

NavButton 组件为例,点击按钮跳转到 /about

import { fireEvent, render } from '@vue/test-utils'
import NavButton from './NavButton.vue'
test('点击按钮跳转到关于页', async () => {
  const { getByText } = render(NavButton, { app, router }) // app 和 router 需提前配置
  await fireEvent.click(getByText('去关于页'))
  expect(router.currentRoute.value.path).toBe('/about')
})

导航守卫的跳转逻辑测试

以全局前置守卫 beforeEach 为例,模拟“未登录访问需授权路由跳转到登录页”:

// 全局守卫逻辑:未登录且路由需授权时,跳转到登录页
router.beforeEach((to, from, next) => {
  const requiresAuth = to.meta.requiresAuth
  if (requiresAuth && !isLoggedIn()) { 
    next({ name: 'Login' })
  } else {
    next()
  }
})
test('未登录访问需授权路由跳转到登录页', async () => {
  vi.mock('./auth', () => ({ isLoggedIn: () => false })) // 模拟未登录状态
  await router.push({ name: 'Profile', meta: { requiresAuth: true } })
  expect(router.currentRoute.value.name).toBe('Login')
})

动态路由参数测试

/user/:id 路由为例,验证参数传递与组件响应:

test('跳转到用户详情页带参数', async () => {
  await router.push({ name: 'User', params: { id: '123' } })
  expect(router.currentRoute.value.params.id).toBe('123')
  // 测试组件接收参数:User 组件用 useRoute 拿到 id 并渲染
  const wrapper = mount(User, { app, router })
  expect(wrapper.vm.userId).toBe('123') // 假设组件内将 route.params.id 赋值给 userId
})

跳转测试的核心是:模拟用户操作/程序触发,检查 router.currentRoute 变化,同时验证组件渲染后的状态

路由懒加载组件的测试坑怎么填?

Vue Router 常用 () => import('./views/About.vue') 实现懒加载,但 Vitest 默认难处理动态 import,需通过 mock 或配置解决。

静态导入 + mock 懒加载

测试时将懒加载组件替换为静态导入,再 mock 生产环境的懒加载逻辑:

// 生产环境路由:{ path: '/about', component: () => import('./views/About.vue') }
import About from './views/About.vue' // 测试时静态导入
test('测试关于页路由', () => {
  vi.mock('vue-router', () => {
    const original = vi.requireActual('vue-router')
    return {
      ...original,
      createRouter: (...args) => {
        const router = original.createRouter(...args)
        // 替换懒加载组件为静态导入的 About
        router.options.routes.find(route => route.name === 'About').component = About
        return router
      }
    }
  })
  // 后续创建 router 并测试...
})

等待组件加载完成

懒加载是异步过程,需用 router.isReady() 等待路由准备好(包括组件加载)后再断言:

test('懒加载组件是否加载成功', async () => {
  await router.push('/about')
  await router.isReady() // 等待异步组件加载完成
  const route = router.currentRoute.value
  expect(route.matched.length).toBe(1) // 确保路由匹配到组件
})

懒加载测试的关键是让测试环境支持异步导入,要么 mock 成静态组件,要么配置 Vitest 支持动态 import,同时需等待加载完成后断言。

路由守卫(导航守卫)的测试逻辑怎么设计?

导航守卫分全局守卫beforeEachafterEach)、路由独享守卫beforeEnter)、组件内守卫onBeforeRouteUpdate),测试需覆盖不同场景下的业务逻辑。

全局前置守卫 beforeEach 测试

以“权限控制”为例,验证不同用户角色的导航结果:

// 全局守卫:用户角色不匹配时跳转到 403
router.beforeEach((to, from, next) => {
  const requiredRole = to.meta.requiredRole
  if (requiredRole && !currentUser.hasRole(requiredRole)) {
    next({ name: 'Forbidden' })
  } else {
    next()
  }
})
test('普通用户访问管理员路由跳转到 403', async () => {
  vi.mock('./user', () => ({ currentUser: { hasRole: () => false } })) // 模拟普通用户
  await router.push({ name: 'Admin', meta: { requiredRole: 'admin' } })
  expect(router.currentRoute.value.name).toBe('Forbidden')
})

路由独享守卫 beforeEnter 测试

以“参数合法性验证”为例,验证参数错误时的导航逻辑:

// 路由配置:产品id不合法时跳转到错误页
{
  path: '/product/:id',
  name: 'Product',
  component: Product,
  beforeEnter: (to, from, next) => {
    const productId = to.params.id
    if (!isValidProductId(productId)) { 
      next({ name: 'Error', params: { code: 400 } })
    } else {
      next()
    }
  }
}
test('产品id不合法跳转到错误页', async () => {
  await router.push({ name: 'Product', params: { id: 'abc' } })
  expect(router.currentRoute.value.name).toBe('Error')
  expect(router.currentRoute.value.params.code).toBe(400)
})

组件内守卫 onBeforeRouteUpdate 测试

以“路由参数变化时刷新数据”为例,验证组件逻辑:

// Product 组件内逻辑:参数变化时调用 fetchProduct
import { onBeforeRouteUpdate } from 'vue-router'
export default {
  setup() {
    const fetchProduct = (id) => { /* 拉取产品数据 */ }
    onBeforeRouteUpdate((to) => {
      fetchProduct(to.params.id)
    })
  }
}
test('路由参数变化时触发数据刷新', async () => {
  const fetchProduct = vi.fn()
  vi.mock('./Product.vue', () => ({
    default: {
      setup() {
        onBeforeRouteUpdate((to) => {
          fetchProduct(to.params.id)
        })
      }
    }
  })) 
  await router.push({ name: 'Product', params: { id: '1' } })
  await router.push({ name: 'Product', params: { id: '2' } }) // 触发参数变化
  expect(fetchProduct).toHaveBeenCalledWith('2')
})

导航守卫测试的核心是模拟输入条件(用户状态、参数等),触发导航后断言业务逻辑(跳转、函数调用等)是否生效

怎么保证每个测试用例的路由环境互不干扰?

若多个用例共用 Router 实例,前一个用例的状态(如 currentRoute、守卫配置)会污染后续用例,需做环境隔离

beforeEach 重置 Router

每个用例执行前,重新创建 Router 实例,确保配置与状态全新:

let router
beforeEach(() => {
  router = createRouter({
    history: createMemoryHistory(),
    routes: [/* 每次重新定义路由表 */]
  })
  // 若有全局守卫,也需重新添加
  router.beforeEach((to, from, next) => { /* 新的守卫逻辑 */ })
})
test('用例1', () => { /* 使用当前 router,状态不影响下一个用例 */ })
test('用例2', () => { /* 新的 router 实例,环境干净 */ })

Vitest mock 重置

若项目封装了 Router(如 router/index.js),用 vi.mock 重置导出,保证每个用例拿到新实例:

vi.mock('../src/router', () => {
  const original = vi.requireActual('../src/router')
  return {
    ...original,
    createRouter: () => original.createRouter({
      history: createMemoryHistory(),
      routes: [/* 测试用路由表 */]
    })
  }
})
test('测试路由', () => {
  const router = require('../src/router').default // 每次 mock 后都是新实例
})

测试完组件需调用 wrapper.unmount() 销毁实例,避免副作用(如路由监听回调)影响其他用例。

动态路由和查询参数的测试怎么落地?

动态路由(/user/:id)与查询参数(?page=2)是路由传参的核心场景,测试需验证参数传递组件响应

动态路由参数测试

验证参数传递与组件逻辑:

test('动态路由参数传递正确', async () => {
  await router.push({ name: 'User', params: { userId: '100' } })
  expect(router.currentRoute.value.params.userId).toBe('100')
  // 测试组件:User 组件用 userId 请求数据
  const wrapper = mount(User, { app, router })
  expect(wrapper.vm.fetchUser).toHaveBeenCalledWith('100') // 假设 fetchUser 是 mock 方法
})

查询参数测试

验证参数变化时组件的响应(如分页逻辑):

test('查询参数变化时更新分页', async () => {
  const updatePagination = vi.fn()
  vi.mock('./List.vue', () => ({
    default: {
      setup() {
        const route = useRoute()
        watch(route.query, (newQuery) => {
          updatePagination(newQuery.page, newQuery.size)
        })
      }
    }
  }))
  await router.push({ path: '/list', query: { page: '1', size: '20' } })
  await router.push({ path: '/list', query: { page: '2', size: '10' } }) // 触发参数变化
  expect(updatePagination).toHaveBeenCalledWith('2', '10')
})

参数类型转换测试

若组件需将路由参数(字符串)转为数字,需验证转换逻辑:

// 组件内逻辑:const page = Number(route.query.page)
test('查询参数转数字类型', () => {
  const wrapper = mount(List, { app, router })
  router.push({ query: { page: '3' } })
  expect(wrapper.vm.page).toBe(3) // 假设组件内 page 是响应式变量
})

动态路由与查询参数测试的核心是覆盖参数传递、组件对参数的读取与处理逻辑,还要考虑边界情况(如参数缺失、格式错误)。

路由元信息(meta)的业务逻辑怎么测?

路由元信息(route.meta)常用于存储权限、页面标题、缓存规则等,测试需验证这些规则是否生效。

由 meta 控制

全局守卫 afterEach 中设置文档标题,测试标题变化:

// 全局守卫:路由切换时设置文档标题
router.afterEach((to) => {
  document.title = to.meta.title || '默认标题'
})
test('路由切换时设置文档标题', async () => {
  const routes = [
    { path: '/', meta: { title: '首页' } },
    { path: '/about', meta: { title: '关于我们' } }
  ]
  const router = createRouter({ history: createMemoryHistory(), routes })
  await router.push('/about')
  expect(document.title).toBe('关于我们')
  await router.push('/')
  expect(document.title).toBe('首页')
})

权限控制(meta.requiresAuth)

测试不同登录状态下的导航结果:

// 全局守卫:需授权路由未登录时跳转到登录页
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next({ name: 'Login' })
  } else {
    next()
  }
})
test('需授权路由未登录跳转到登录页', async () => {
  vi.mock('./auth', () => ({ isLoggedIn: () => false }))
  const routes = [{ path: '/profile', name: 'Profile', meta: { requiresAuth: true } }]
  const router = createRouter({ history: createMemoryHistory(), routes })
  await router.push('/profile')
  expect(router.currentRoute.value.name).toBe('Login')
})

组件缓存(meta.keepAlive)

测试带 keepAlive 的路由组件是否被缓存(如 created 钩子执行次数):

test('带 keepAlive 的路由组件是否缓存', async () => {
  const routes = [{ path: '/cache', name: 'Cache', component: Cache, meta: { keepAlive: true } }]
  const router = createRouter({ history: createMemoryHistory(), routes })
  const wrapper1 = mount(Cache, { app, router })
  expect(Cache.created).toHaveBeenCalledTimes(1) // 首次创建执行 created
  await router.push('/other')
  await router.push('/cache')
  const wrapper2 = mount(Cache, { app, router })
  expect(Cache.created).toHaveBeenCalledTimes(1) // 缓存后 created 不执行
})

路由元信息测试的核心是明确 meta 对应的业务规则(权限、标题等),触发导航后断言规则是否生效

把 Vue Router 和 Vitest 结合做测试,核心是「还原路由运行的真实环境」+「精准覆盖业务逻辑」,从环境搭建到跳转、守卫、懒加载、元信息这些细节,每个环节都要考虑测试场景与边界,只要把路由的“输入(跳转动作、参数)”和“输出(页面变化、逻辑执行)”

版权声明

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

发表评论:

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

热门