SoybeanAdmin单元测试:Jest+Vue Test Utils测试方案
SoybeanAdmin单元测试:Jest+Vue Test Utils测试方案
引言:为什么你的Admin项目需要单元测试?
你是否遇到过这些问题:修改一个按钮组件导致整个表单提交功能瘫痪?升级依赖后控制台报错却找不到原因?重构代码时战战兢兢生怕破坏现有功能?作为基于Vue3+TypeScript的后台管理模板,SoybeanAdmin的组件复用率高、业务逻辑复杂,缺乏单元测试就像在钢丝上行走——每一步都可能坠入深渊。
本文将系统讲解如何使用Jest+Vue Test Utils为SoybeanAdmin构建完整的单元测试体系,读完你将掌握:
- 从零配置Vue3+TypeScript项目的测试环境
- 编写组件测试的最佳实践与常见陷阱
- Pinia状态管理与Vue Router的测试策略
- 实现测试覆盖率100%的自动化方案
测试环境搭建:从零到一配置Jest生态
核心依赖安装
SoybeanAdmin使用pnpm作为包管理器,首先安装测试所需的核心依赖:
pnpm add -D jest @types/jest vue-jest@next @vue/test-utils@next babel-jest ts-jest @babel/preset-env @babel/preset-typescript @vue/babel-plugin-jsx jest-transform-stub
配置文件编写
在项目根目录创建以下配置文件:
jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub'
},
moduleFileExtensions: ['vue', 'ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testMatch: ['**/__tests__/**/*.spec.[jt]s?(x)'],
collectCoverageFrom: [
'src/components/**/*.vue',
'src/hooks/**/*.ts',
'!src/**/*.d.ts',
'!**/node_modules/**'
],
coverageDirectory: 'coverage',
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
babelConfig: true
}
}
}
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
['@vue/babel-plugin-jsx', { enableObjectSlots: false }]
]
}
package.json脚本配置
修改package.json添加测试脚本:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --coverage --ci"
}
}
组件测试实战:从基础到进阶
基础组件测试示例
以src/components/common/button-icon.vue为例,创建测试文件__tests__/components/common/button-icon.spec.ts:
import { describe, expect, it } from '@jest/globals'
import { mount } from '@vue/test-utils'
import ButtonIcon from '@/components/common/button-icon.vue'
import { Icon } from '@/components/custom/svg-icon.vue'
describe('ButtonIcon.vue', () => {
it('renders icon correctly when props.icon is provided', () => {
const icon = 'activity'
const wrapper = mount(ButtonIcon, {
props: { icon }
})
const svgIcon = wrapper.findComponent(Icon)
expect(svgIcon.exists()).toBe(true)
expect(svgIcon.props('name')).toBe(icon)
})
it('triggers click event when clicked', async () => {
const handleClick = jest.fn()
const wrapper = mount(ButtonIcon, {
props: {
icon: 'activity',
onClick: handleClick
}
})
await wrapper.trigger('click')
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('applies correct size class based on size prop', () => {
const wrapper = mount(ButtonIcon, {
props: {
icon: 'activity',
size: 'large'
}
})
expect(wrapper.classes()).toContain('button-icon-large')
})
})
复杂组件测试策略
对于包含Pinia状态管理的组件,如src/views/manage/menu/index.vue,需要使用Pinia测试工具:
import { describe, expect, it, vi } from '@jest/globals'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import MenuManage from '@/views/manage/menu/index.vue'
import { useMenuStore } from '@/store/modules/menu'
describe('MenuManage.vue', () => {
it('fetches menu list on mount', async () => {
const mockMenus = [
{ id: 1, name: 'Dashboard', path: '/dashboard' },
{ id: 2, name: 'User Management', path: '/users' }
]
const wrapper = mount(MenuManage, {
global: {
plugins: [createTestingPinia({
initialState: { menu: { menuList: mockMenus } }
})]
}
})
const menuStore = useMenuStore()
expect(menuStore.getMenuList).toHaveBeenCalled()
expect(wrapper.find('.menu-item').exists()).toBe(true)
expect(wrapper.findAll('.menu-item').length).toBe(2)
})
it('opens modal when clicking add button', async () => {
const wrapper = mount(MenuManage, {
global: {
plugins: [createTestingPinia()]
}
})
await wrapper.find('.add-menu-btn').trigger('click')
expect(wrapper.findComponent({ name: 'MenuOperateModal' }).exists()).toBe(true)
})
})
测试覆盖率提升技巧
| 覆盖率类型 | 含义 | 目标值 | 实现策略 |
|---|---|---|---|
| 语句覆盖率(Statement) | 执行到的代码语句比例 | ≥90% | 确保每个条件分支都被测试 |
| 分支覆盖率(Branch) | 条件分支被执行的比例 | ≥85% | 使用不同输入测试if/else、switch分支 |
| 函数覆盖率(Function) | 被调用的函数比例 | ≥95% | 测试所有导出函数和组件方法 |
| 行覆盖率(Line) | 被执行的代码行比例 | ≥90% | 避免冗余代码,针对性编写测试 |
Pinia状态管理测试
Store单元测试
测试src/store/modules/tab/index.ts:
import { describe, expect, it, vi, beforeEach } from '@jest/globals'
import { setActivePinia, createPinia } from 'pinia'
import { useTabStore } from '@/store/modules/tab'
import { Tab } from '@/typings/common'
describe('Tab Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('adds new tab correctly', () => {
const tabStore = useTabStore()
const newTab: Tab = {
path: '/test',
name: 'Test',
component: 'test.vue',
meta: { title: 'Test Page' }
}
tabStore.addTab(newTab)
expect(tabStore.tabList.length).toBe(1)
expect(tabStore.tabList[0]).toEqual(newTab)
})
it('removes tab and switches to previous when closing current tab', () => {
const tabStore = useTabStore()
const tab1: Tab = { path: '/tab1', name: 'Tab1', component: 'tab1.vue', meta: { title: 'Tab 1' } }
const tab2: Tab = { path: '/tab2', name: 'Tab2', component: 'tab2.vue', meta: { title: 'Tab 2' } }
tabStore.addTab(tab1)
tabStore.addTab(tab2)
tabStore.setCurrentTab(tab2.path)
tabStore.closeTab(tab2.path)
expect(tabStore.tabList.length).toBe(1)
expect(tabStore.currentTab).toBe(tab1.path)
})
})
路由测试
路由守卫测试
测试src/router/guard/route.ts中的路由守卫:
import { describe, expect, it, vi } from '@jest/globals'
import { createRouter, createMemoryHistory } from 'vue-router'
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
import { beforeEachGuard } from '@/router/guard/route'
import { useAuthStore } from '@/store/modules/auth'
vi.mock('@/store/modules/auth')
describe('Route Guard', () => {
it('redirects to login when accessing protected route without auth', async () => {
const authStore = { isLogin: false }
;(useAuthStore as jest.Mock).mockReturnValue(authStore)
const router = createRouter({
history: createMemoryHistory(),
routes: setupLayouts(generatedRoutes)
})
router.beforeEach(beforeEachGuard)
const navigate = await router.push('/manage/menu')
await router.isReady()
expect(router.currentRoute.value.path).toBe('/login')
})
it('allows access to protected route when authenticated', async () => {
const authStore = { isLogin: true }
;(useAuthStore as jest.Mock).mockReturnValue(authStore)
const router = createRouter({
history: createMemoryHistory(),
routes: setupLayouts(generatedRoutes)
})
router.beforeEach(beforeEachGuard)
await router.push('/manage/menu')
await router.isReady()
expect(router.currentRoute.value.path).toBe('/manage/menu')
})
})
测试集成与CI/CD
GitHub Actions配置
创建.github/workflows/test.yml:
name: Unit Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test:ci
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage-final.json
测试报告分析
Jest生成的覆盖率报告包含以下关键文件:
coverage/
├── clover.xml # Clover格式报告,适合Jenkins等CI工具
├── coverage-final.json # 原始覆盖率数据
├── lcov-report/ # HTML报告目录
│ ├── index.html # 覆盖率总览
│ ├── src/ # 按目录结构的详细覆盖率
│ └── ...
└── lcov.info # LCOV格式报告,适合Codecov等服务
常见问题与解决方案
测试Vue单文件组件的常见问题
| 问题 | 解决方案 | 示例代码 |
|---|---|---|
| 样式导入错误 | 使用jest-transform-stub处理样式文件 | moduleNameMapper: { '\\.(css|scss)$': 'jest-transform-stub' } |
| 组件异步加载 | 使用async/await等待组件渲染 | const wrapper = await mount(AsyncComponent) |
| TypeScript类型错误 | 配置ts-jest和正确的tsconfig | "ts-jest": { "tsconfig": "tsconfig.test.json" } |
| 第三方组件测试 | 使用stubs或mocks简化测试 | mount(Component, { global: { stubs: ['ElButton'] } }) |
测试性能优化
- 使用test.only和test.skip:开发时只运行相关测试
- 配置testMatch:精确匹配测试文件,避免不必要的搜索
- 使用jest-worker:多进程并行运行测试
- 优化大型测试文件:拆分为多个小测试文件
- 模拟网络请求:使用nock或jest.mock模拟API调用
总结与最佳实践
测试金字塔在SoybeanAdmin中的应用
测试驱动开发(TDD)工作流
最终项目测试结构
src/
├── __tests__/ # 测试目录
│ ├── components/ # 组件测试
│ ├── hooks/ # 钩子测试
│ ├── store/ # 状态管理测试
│ ├── router/ # 路由测试
│ └── utils/ # 工具函数测试
├── ...
└── vitest.config.ts # 测试配置
通过本文介绍的Jest+Vue Test Utils测试方案,你可以为SoybeanAdmin构建健壮的测试体系,显著降低回归错误风险,提高代码质量和开发效率。记住,好的测试不仅是质量保障,更是团队协作和项目可维护性的基石。
立即行动:
- 按照本文步骤配置测试环境
- 为核心组件编写基础测试用例
- 将测试集成到你的CI/CD流程
- 逐步提升测试覆盖率至90%以上
关注项目仓库获取更多测试示例和最佳实践,下一篇我们将深入探讨端到端测试和性能测试方案。
本文测试方案已在SoybeanAdmin v2.5.0及以上版本验证通过,低版本可能需要调整配置。
更多推荐

所有评论(0)