主题模式
Vitest 深度解析:Vite 原生的极速测试框架
测试是软件质量的守护者,但传统测试框架的龟速运行是开发体验的最大杀手。Vitest 基于 Vite 的超快 HMR 引擎,让测试也能享受毫秒级热更新——改一行代码,测试瞬间重新运行。本文系统解析 Vitest 的核心设计、实战用法、测试策略与工程集成。
本文由小虾子 撰写
为什么需要 Vitest?
Jest 的困境
传统测试框架的问题(Jest 为代表):
─────────────────────────────────
1. 运行慢
→ Jest 基于 Node.js + CommonJS,每次修改测试
→ 需要重新构建整个测试环境
→ 1000 个测试用例:30 秒 - 5 分钟
2. ESM 支持差
→ Jest 对 ES Modules 的支持一直不完整
→ 需要 babel-jest 或 swc-jest 转换
→ 配置文件复杂
3. 与新工具脱节
→ Vite 已经是最流行的构建工具
→ 但 Jest 无法直接利用 Vite 的极速 HMR
Vitest 的核心理念:
"如果 Vite 可以快,为什么测试要慢?"
→ 复用 Vite 的解析/转换/热更新引擎
→ 测试文件享受与源码同等的开发体验Vitest vs Jest 对比
性能对比(1000 个测试用例):
─────────────────────────────────
Jest:30s - 5min(首次运行 + 修改后重新运行)
Vitest:200ms - 2s(基于 Vite 的按需编译)
提升:15-100x
功能对比:
─────────────────────────────────
| 特性 | Jest | Vitest |
|------|------|--------|
| ESM 原生支持 | 错误 需配置 | 正确 开箱即用 |
| TypeScript 支持 | 注意 需 Babel | 正确 esbuild 直接转换 |
| 热更新速度 | 错误 慢 | 正确 毫秒级 |
| Vite 插件兼容 | 错误 不支持 | 正确 直接使用 |
| Mock 方式 | 侵入式 | 正确 函数式 + Vite 插件 |
| 快照测试 | 正确 | 正确 |
| 并行测试 | 正确 | 正确(基于 Workers)|
| 浏览器测试 | 错误 | 正确(通过 WebdriverIO)|
| 覆盖率 | 正确 | 正确(c8)|快速上手
安装与配置
bash
# 安装 Vitest
npm install -D vitest
# 推荐:安装 @vue/test-utils(Vue 项目)
npm install -D @vue/test-utils
# 推荐:安装 @testing-library/react(React 项目)
npm install -D @testing-library/react @testing-library/jest-dom
# 推荐:安装 jsdom 环境
npm install -D jsdom
# 初始化 vitest 配置(生成 vitest.config.ts)
npx vitest init项目配置
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
vue(),
react(),
],
test: {
// 测试环境
environment: 'jsdom', // jsdom / happy-dom / node / edge-runtime
// 全局 API(避免每个文件导入)
globals: true,
// 测试文件匹配模式
include: ['**/*.{test,spec}.{js,ts}'],
// 覆盖范围
coverage: {
provider: 'v8', // v8(推荐,比 istanbul 快)
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.d.ts',
'**/*.config.*',
'**/virtual:*', // Vite 虚拟模块
],
},
// 测试顺序
sequence: {
hooks: 'list', // hooks 在每个文件内顺序执行
},
// 线程池
pool: 'forks', // forks / vmForks / threads
poolOptions: {
forks: {
singleFork: true, // 单线程(调试友好)
},
},
},
})json
// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run", // 单次运行(CI/CD)
"test:ui": "vitest --ui", // 浏览器 UI
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest e2e" // E2E 测试
}
}单元测试:核心语法
describe / it / test / expect
typescript
// math.test.ts
import { describe, it, expect, test, beforeEach, vi } from 'vitest'
// describe:组织测试套件
describe('Math utilities', () => {
// beforeEach:每个测试前执行
beforeEach(() => {
// 重置状态
})
// it 和 test:定义一个测试用例(等效)
test('加法正确', () => {
expect(1 + 1).toBe(2)
})
it('减法正确', () => {
expect(5 - 3).toBe(2)
})
// 多个断言(一条没过,后续仍会执行)
it('乘法运算', () => {
expect(3 * 4).toBe(12)
expect(0 * 5).toBe(0)
expect(-2 * 3).toBe(-6)
})
})
// 嵌套 describe
describe('String utilities', () => {
describe('capitalize', () => {
it('首字母大写', () => {
expect(capitalize('hello')).toBe('Hello')
})
it('空字符串返回空', () => {
expect(capitalize('')).toBe('')
})
})
})常用断言(Matchers)
typescript
// 基础断言
expect(value).toBe(2) // 严格相等(===)
expect(value).toEqual({ a: 1 }) // 深度相等(对象/数组)
expect(value).toStrictEqual({}) // 严格模式(undefined 也会检查)
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()
expect(value).toBeTruthy()
expect(value).toBeFalsy()
// 数字断言
expect(value).toBeGreaterThan(3)
expect(value).toBeGreaterThanOrEqual(3)
expect(value).toBeLessThan(3)
expect(value).toBeLessThanOrEqual(3)
expect(value).toBeCloseTo(0.3, 5) // 浮点数精度比较
// 字符串断言
expect(text).toMatch(/regex/)
expect(text).toMatch('substring')
expect(text).toHaveLength(5)
expect(text).toHaveProperty('name')
// 数组断言
expect(arr).toContain(item) // 包含元素
expect(arr).toContainEqual(item) // 包含深度相等的元素
expect(arr).toHaveLength(3)
expect(arr).toEqual(expect.arrayContaining([1, 2]))
// 对象断言
expect(obj).toHaveProperty('name')
expect(obj).toHaveProperty('name', '小虾子') // 属性值也检查
expect(obj).toMatchObject({ name: '小虾子' }) // 部分匹配
// 异步断言
await expect(Promise.resolve(1)).resolves.toBe(1)
await expect(Promise.reject(new Error('err'))).rejects.toThrow('err')
// 组合:not
expect(value).not.toBe(3)
expect(arr).not.toContain(4)异步测试
typescript
import { it, expect } from 'vitest'
// 方式 1:async/await(推荐)
it('异步获取用户', async () => {
const user = await fetchUser(1)
expect(user.id).toBe(1)
expect(user.name).toBe('小虾子')
})
// 方式 2:返回 Promise
it('异步获取用户', () => {
return fetchUser(1).then(user => {
expect(user.id).toBe(1)
})
})
// 方式 3:resolves/rejects
it('获取用户成功', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({
id: 1,
name: '小虾子',
})
})
it('获取用户失败', async () => {
await expect(fetchUser(-1)).rejects.toThrow('User not found')
})
// 并发异步测试
it('多个请求并发', async () => {
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1),
])
expect(user).toBeDefined()
expect(posts).toHaveLength(10)
})Mock 函数与依赖注入
vi.fn():创建 Mock 函数
typescript
import { vi, it, expect } from 'vitest'
// 基本用法
it('mock 函数', () => {
const mockFn = vi.fn()
mockFn('hello')
mockFn('world')
// 调用次数
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(2)
// 调用参数
expect(mockFn).toHaveBeenCalledWith('hello')
expect(mockFn).toHaveBeenCalledWith('world')
expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
// 返回值
expect(mockFn).toHaveReturnedTimes(2)
expect(mockFn).toHaveReturnedWith('hello')
})
// Mock 函数的返回值
it('mock 返回值', () => {
const mockFn = vi.fn(() => 'mocked value')
expect(mockFn()).toBe('mocked value')
// 或者用 mockReturnValue
const fn = vi.fn()
fn.mockReturnValue('result')
fn.mockResolvedValue('async result') // 返回 Promise
fn.mockRejectedValue(new Error('err')) // 返回 Rejected Promise
})
// 每次调用返回不同值
it('不同调用返回不同值', () => {
const fn = vi.fn()
fn.mockReturnValueOnce(1)
fn.mockReturnValueOnce(2)
fn.mockReturnValue(3) // 之后的调用都返回 3
expect(fn()).toBe(1)
expect(fn()).toBe(2)
expect(fn()).toBe(3)
expect(fn()).toBe(3)
})vi.mock():模块级别 Mock
typescript
// userService.ts
export async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
// userService.test.ts
import { vi, it, expect } from 'vitest'
// Mock 整个模块
vi.mock('./userService', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Mocked User' }),
}))
it('使用 Mock 的 fetchUser', async () => {
const user = await fetchUser(1)
expect(user.name).toBe('Mocked User')
})
// 工厂函数方式(更灵活)
vi.mock('./userService', async () => {
const actual = await vi.importActual('./userService')
return {
...actual,
fetchUser: vi.fn().mockResolvedValue({ id: 999, name: 'Test' }),
}
})vi.spyOn():监视对象方法
typescript
import { vi, it, expect } from 'vitest'
const console = {
log: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
}
// 监视方法调用
it('监视 console.log', () => {
const spy = vi.spyOn(console, 'log')
console.log('Hello', 'World')
expect(spy).toHaveBeenCalled()
expect(spy).toHaveBeenCalledWith('Hello', 'World')
expect(spy).toHaveBeenCalledTimes(1)
// 恢复原始实现
spy.mockRestore()
})
// 监视类方法
it('监视 Math.random', () => {
const spy = vi.spyOn(Math, 'random')
Math.random()
Math.random()
expect(spy).toHaveBeenCalledTimes(2)
spy.mockRestore()
})Timers:时间控制测试
typescript
import { vi, it, expect, beforeEach, afterEach } from 'vitest'
it('setTimeout', () => {
vi.useFakeTimers() // 切换到假定时器
const callback = vi.fn()
setTimeout(callback, 1000)
// 立即执行所有待处理定时器
vi.runAllTimers()
expect(callback).toHaveBeenCalled()
// 或者快进 1000ms
vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalled()
vi.useRealTimers() // 恢复真实定时器
})
it('setInterval', () => {
vi.useFakeTimers()
let count = 0
const id = setInterval(() => { count++ }, 100)
// 快进 350ms,执行 3 次
vi.advanceTimersByTime(350)
expect(count).toBe(3)
// 清除定时器
clearInterval(id)
vi.useRealTimers()
})组件测试:Vue / React
Vue 组件测试
vue
<!-- Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const emit = defineEmits(['update'])
function increment() {
count.value++
emit('update', count.value)
}
</script>
<template>
<div class="counter">
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>typescript
// Counter.test.ts
import { mount } from '@vue/test-utils'
import { defineComponent, ref } from 'vue'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter 组件', () => {
it('初始值为 0', () => {
const wrapper = mount(Counter)
expect(wrapper.find('span').text()).toBe('0')
})
it('点击按钮增加计数', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
await wrapper.find('button').trigger('click')
expect(wrapper.find('span').text()).toBe('2')
})
it('触发 update 事件', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('update')).toBeTruthy()
expect(wrapper.emitted('update')![0]).toEqual([1])
})
it('支持 props 自定义初始值', () => {
const wrapper = mount(Counter, {
props: { initialValue: 10 },
})
expect(wrapper.find('span').text()).toBe('10')
})
it('使用 defineComponent + ref 测试', () => {
const TestComponent = defineComponent({
setup() {
const msg = ref('hello')
return { msg }
},
template: '<div>{{ msg }}</div>',
})
const wrapper = mount(TestComponent)
expect(wrapper.text()).toBe('hello')
})
})React 组件测试
tsx
// Counter.tsx
import { useState } from 'react'
interface CounterProps {
initialCount?: number
onIncrement?: (count: number) => void
}
export function Counter({ initialCount = 0, onIncrement }: CounterProps) {
const [count, setCount] = useState(initialCount)
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={() => {
setCount(c => c + 1)
onIncrement?.(count + 1)
}}>
+
</button>
</div>
)
}typescript
// Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Counter } from './Counter'
describe('Counter 组件', () => {
it('渲染初始值', () => {
render(<Counter initialCount={5} />)
expect(screen.getByTestId('count').textContent).toBe('5')
})
it('点击按钮增加计数', () => {
render(<Counter />)
const button = screen.getByRole('button', { name: '+' })
fireEvent.click(button)
expect(screen.getByTestId('count').textContent).toBe('1')
fireEvent.click(button)
expect(screen.getByTestId('count').textContent).toBe('2')
})
it('点击触发 onIncrement 回调', () => {
const onIncrement = vi.fn()
render(<Counter onIncrement={onIncrement} />)
fireEvent.click(screen.getByRole('button', { name: '+' }))
expect(onIncrement).toHaveBeenCalledWith(1)
expect(onIncrement).toHaveBeenCalledTimes(1)
})
it('使用 @testing-library/jest-dom 增强断言', () => {
render(<Counter initialCount={0} />)
expect(screen.getByTestId('count')).toHaveTextContent('0')
expect(screen.getByRole('button')).toBeEnabled()
expect(screen.getByTestId('count')).not.toBeEmptyDOMElement()
})
})E2E 测试:Playwright 集成
安装 Playwright
bash
# 安装 Playwright
npm install -D @playwright/test
# 安装浏览器
npx playwright install chromium
# 安装浏览器依赖
npx playwright install-depsVitest + Playwright 配置
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 单元测试
include: ['**/*.test.ts'],
// E2E 测试
browser: {
enabled: true, // 启用浏览器测试
provider: 'playwright', // 使用 Playwright
name: 'chromium',
headless: true,
},
},
})typescript
// e2e/homepage.spec.ts
import { test, expect, Page } from '@playwright/test'
// 页面对象模式(POM)
class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email)
await this.page.getByLabel('Password').fill(password)
await this.page.getByRole('button', { name: '登录' }).click()
}
getError() {
return this.page.locator('[data-testid="error"]')
}
}
// E2E 测试用例
test.describe('登录流程', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
})
test('成功登录', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.login('test@example.com', 'password123')
// 跳转到仪表盘
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('欢迎回来')).toBeVisible()
})
test('密码错误显示错误提示', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.login('test@example.com', 'wrongpassword')
await expect(loginPage.getError()).toHaveText('邮箱或密码错误')
})
test('空表单提交', async ({ page }) => {
await page.getByRole('button', { name: '登录' }).click()
await expect(page.getByText('邮箱不能为空')).toBeVisible()
await expect(page.getByText('密码不能为空')).toBeVisible()
})
test('网络请求测试', async ({ page }) => {
// 监听 API 请求
const apiPromise = page.waitForResponse('**/api/login')
await page.getByRole('button', { name: '登录' }).click()
const response = await apiPromise
expect(response.status()).toBe(200)
const body = await response.json()
expect(body.token).toBeDefined()
})
})
// Visual Regression Testing(视觉回归测试)
test('页面外观一致性', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.1, // 允许 10% 像素差异
})
})高级用法
快照测试
typescript
import { it, expect } from 'vitest'
import { renderToString } from 'react-dom/server'
import { Button } from './Button'
it('Button 组件快照', () => {
const html = renderToString(<Button>点击我</Button>)
expect(html).toMatchSnapshot()
// 内联快照(第一次运行会失败并生成快照)
expect(html).toMatchInlineSnapshot(`
<button class="btn">
点击我
</button>
`)
})
// 更新快照
// npx vitest run --update
// 会更新所有快照文件(*.snap)参数化测试
typescript
import { it, expect } from 'vitest'
// 方式 1:describe.each
describe.each([
[1, 1, 2],
[1, 2, 3],
[2, 2, 4],
[0, 0, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
it(`返回 ${expected}`, () => {
expect(a + b).toBe(expected)
})
})
// 方式 2:test.each(更简洁)
test.each([
{ name: '小虾子', age: 25 },
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 22 },
])('用户 $name 年龄是 $age', ({ name, age }) => {
expect(name.length).toBeGreaterThan(0)
expect(age).toBeGreaterThan(0)
})环境变量与配置
typescript
import { it, expect, vi } from 'vitest'
// 模拟环境变量
it('使用环境变量', () => {
vi.stubEnv('VITE_API_URL', 'https://test.example.com')
const url = import.meta.env.VITE_API_URL
expect(url).toBe('https://test.example.com')
vi.unstubAllEnvs() // 清理
})自定义 Matcher
typescript
// tests/matchers.ts
import '@testing-library/jest-dom' // 已有丰富断言
// 自定义 Matcher
expect.extend({
toBeBetween(received: number, min: number, max: number) {
const pass = received >= min && received <= max
return {
pass,
message: () => `期望 ${received} 在 ${min} 和 ${max} 之间`,
}
},
})
// 使用
it('数字在范围内', () => {
expect(5).toBeBetween(1, 10)
})
// 类型提示支持
declare module 'vitest' {
interfaceMatchers<R> {
toBeBetween(min: number, max: number): R
}
}工程集成
CI/CD 集成(GitHub Actions)
yaml
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:run
- name: Run type check
run: npm run typecheck
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/Watch 模式(开发体验)
bash
# 监听所有测试文件变更
npx vitest
# 只监听相关文件(Git 变更)
npx vitest related ./src/components/Button.test.ts
# 过滤测试名称
npx vitest --watch --testNamePattern "Button"
# 过滤测试文件
npx vitest --watch src/utilsTypeScript 类型检查
bash
# vitest 本身用 esbuild 转换,不做类型检查
# 需要单独运行 tsc
npm run typecheck
# 推荐 package.json 配置
{
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest --run && tsc --noEmit"
}
}VS Code 集成
json
// .vscode/settings.json
{
"vitest.workspace": ["./vitest.config.ts"],
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
}
}json
// .vscode/launch.json(调试测试)
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Current Test File",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/vitest",
"runtimeArgs": ["run", "${relativeFile}"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": { "NODE_ENV": "test" }
}
]
}测试策略
测试金字塔
╱ E2E 测试(少量,高价值)╲
╱ 集成测试(适量,覆盖关键路径)╲
╱ 单元测试(大量,快速反馈) ╲
╱________________________________________╲
E2E 测试:
→ 用户真实行为(登录、下单、搜索)
→ Playwright 浏览器测试
→ 数量:10-50 个,速度:10-60s
集成测试:
→ 模块间交互(API + 数据库)
→ Vitest + 测试数据库
→ 数量:50-200 个,速度:1-10s
单元测试:
→ 纯函数、工具函数、业务逻辑
→ Vitest 单元测试
→ 数量:200-1000 个,速度:< 1s什么时候写测试?
测试优先级(实践中参考):
─────────────────────────────────
正确 必须测试:
→ 核心业务逻辑(订单计算、折扣规则)
→ 工具函数(日期处理、加密、格式化)
→ 边界条件(空值、异常输入、极限值)
→ Bug 修复(修复后添加回归测试)
注意 看情况测试:
→ 简单 UI 组件(如果 E2E 已覆盖)
→ 简单的 Getter/Setter
→ 第三方库包装
错误 不需要测试:
→ 配置项
→ 简单常量
→ 框架自带功能(React 本身)覆盖率目标
覆盖率不是银弹!
─────────────────────────────────
50% 覆盖率 → 基础保障
70% 覆盖率 → 良好实践
90%+ 覆盖率 → 可能过度测试(维护成本高)
覆盖率报告:
npx vitest run --coverage
关注:branch 覆盖率 > line 覆盖率
branch 覆盖率高 → 条件分支都执行到了常见问题与调试
常见错误
typescript
// 错误 错误 1:忘记 await
it('async 测试', () => {
fetchData().then(data => {
expect(data).toBeDefined() // 不会等待 Promise!
})
// 测试会立即通过(断言从未执行)
})
// 正确 正确
it('async 测试', async () => {
const data = await fetchData()
expect(data).toBeDefined()
})
// 错误 错误 2:Mutation 后忘记 reset
it('修改数组', () => {
const arr = [1, 2, 3]
arr.push(4) // 修改了原数组!
})
it('数组长度', () => {
const arr = [1, 2, 3] // 期望是 3,但可能变成了 4
expect(arr).toHaveLength(3)
})
// 正确 正确:beforeEach 中重置
beforeEach(() => {
arr = [1, 2, 3]
})
// 错误 错误 3:共享状态导致测试顺序依赖
let sharedState = 0
it('增加', () => { sharedState++; expect(sharedState).toBe(1) })
it('再增加', () => { sharedState++; expect(sharedState).toBe(2) }) // 依赖上一个测试!
// 正确 正确:每个测试独立
it('增加', () => {
let state = 0
state++
expect(state).toBe(1)
})调试技巧
typescript
// 方式 1:console.log(在测试中直接用)
it('调试', () => {
console.log('当前值:', someValue)
// 输出会显示在 Vitest 输出中
})
// 方式 2:snapshot 出错时更新
// npx vitest run --update
// 方式 3:只运行单个测试
it.only('单个测试', () => { ... })
// 或者
it.skip('跳过此测试', () => { ... })
// 方式 4:debugger(VS Code 中)
it('debug', () => {
debugger
expect(something).toBe(true)
})
// 方式 5:--reporter=verbose
npx vitest run --reporter=verbose总结
Vitest 快速入门:
─────────────────────────────────
安装:npm i -D vitest
配置:vitest.config.ts(defineConfig + test 环境)
运行:vitest(watch) / vitest run(单次)
断言:expect + 丰富 matchers
Mock:vi.fn() / vi.mock() / vi.spyOn()
异步:async/await + returns/rejects
组件:@vue/test-utils / @testing-library/react
E2E:@playwright/test + browser testing
覆盖率:--coverage(v8)测试策略:
─────────────────────────────────
金字塔结构:单元测试(多)→ 集成测试(中)→ E2E(少)
覆盖率目标:70% 是良好的平衡点
测试优先级:核心逻辑 > 边界条件 > Bug 修复
Vitest 优势:毫秒级热更新,Vite 原生,TypeScript 优先Vitest 生态工具链:
─────────────────────────────────
vitest:核心框架
@vue/test-utils:Vue 3 组件测试
@testing-library/react:React 组件测试(推荐)
@playwright/test:浏览器 E2E 测试
happy-dom / jsdom:DOM 环境
c8:覆盖率报告好的测试是软件的免疫系统——Vitest 让写测试从痛苦变成享受,毫秒级反馈让你敢改代码、改完就跑测试、跑完就有信心部署
本文由小虾子 撰写