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

一、测试前,先把环境搭对

terry 9小时前 阅读数 10 #Vue
文章标签 测试 环境

很多做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组件有没有渲染?这一步要解决“路由匹配”和“组件渲染”的联动测试。

实操步骤:

  1. 先把路由跳转到目标路径(;
  2. 等路由“就绪”(因为路由跳转是异步的,得用await router.isReady()等它完成);
  3. mount渲染组件,同时注入测试路由;
  4. 断言组件是否存在、内容是否符合预期。

看例子:

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.pushuseRouter().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') // 提前加载异步组件
})

路由异步操作一定要等!

路由的pushisReady都是异步函数,测试时必须用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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门