Vitest 编写测试 #
测试基础结构 #
第一个测试 #
typescript
// sum.ts
export function sum(a: number, b: number): number {
return a + b
}
// sum.test.ts
import { describe, it, expect } from 'vitest'
import { sum } from './sum'
describe('sum function', () => {
it('should add two numbers correctly', () => {
expect(sum(1, 2)).toBe(3)
})
})
测试函数说明 #
| 函数 | 说明 | 用途 |
|---|---|---|
describe |
创建测试套件 | 组织相关测试 |
it |
定义测试用例 | 单个测试 |
test |
it 的别名 |
单个测试 |
expect |
创建断言 | 验证结果 |
基本测试模式 #
typescript
import { describe, it, expect } from 'vitest'
describe('测试套件名称', () => {
it('测试用例描述', () => {
// 准备(Arrange)
const input = 1 + 2
// 执行(Act)
const result = input
// 断言(Assert)
expect(result).toBe(3)
})
})
测试组织 #
嵌套测试套件 #
typescript
import { describe, it, expect } from 'vitest'
describe('Calculator', () => {
describe('addition', () => {
it('should add positive numbers', () => {
expect(1 + 2).toBe(3)
})
it('should add negative numbers', () => {
expect(-1 + -2).toBe(-3)
})
})
describe('subtraction', () => {
it('should subtract numbers', () => {
expect(5 - 3).toBe(2)
})
})
})
测试命名最佳实践 #
typescript
import { describe, it, expect } from 'vitest'
// 好的命名:清晰描述测试意图
describe('UserService', () => {
describe('createUser', () => {
it('should create a user with valid data', () => {
// ...
})
it('should throw error when email is invalid', () => {
// ...
})
it('should throw error when password is too short', () => {
// ...
})
})
})
// 不好的命名:模糊不清
describe('user', () => {
it('test1', () => {
// ...
})
it('works', () => {
// ...
})
})
生命周期钩子 #
四种钩子函数 #
typescript
import { describe, beforeAll, beforeEach, afterEach, afterAll, it } from 'vitest'
describe('Lifecycle Hooks', () => {
// 所有测试之前执行一次
beforeAll(() => {
console.log('beforeAll: 初始化数据库连接')
})
// 每个测试之前执行
beforeEach(() => {
console.log('beforeEach: 重置测试数据')
})
it('test 1', () => {
console.log('test 1')
})
it('test 2', () => {
console.log('test 2')
})
// 每个测试之后执行
afterEach(() => {
console.log('afterEach: 清理测试数据')
})
// 所有测试之后执行一次
afterAll(() => {
console.log('afterAll: 关闭数据库连接')
})
})
// 执行顺序:
// beforeAll -> beforeEach -> test 1 -> afterEach
// -> beforeEach -> test 2 -> afterEach -> afterAll
钩子执行顺序图 #
text
┌─────────────────────────────────────────────────────────────┐
│ describe 块开始 │
├─────────────────────────────────────────────────────────────┤
│ │
│ beforeAll() ────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 测试 1 │ │
│ │ ├── beforeEach() │ │
│ │ ├── 测试代码 │ │
│ │ └── afterEach() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 测试 2 │ │
│ │ ├── beforeEach() │ │
│ │ ├── 测试代码 │ │
│ │ └── afterEach() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ afterAll() ─────────────────────────────────────────────── │
│ │
└─────────────────────────────────────────────────────────────┘
嵌套套件中的钩子 #
typescript
import { describe, beforeAll, beforeEach, afterAll, afterEach, it } from 'vitest'
describe('Outer', () => {
beforeAll(() => console.log('Outer beforeAll'))
beforeEach(() => console.log('Outer beforeEach'))
afterEach(() => console.log('Outer afterEach'))
afterAll(() => console.log('Outer afterAll'))
describe('Inner', () => {
beforeAll(() => console.log('Inner beforeAll'))
beforeEach(() => console.log('Inner beforeEach'))
afterEach(() => console.log('Inner afterEach'))
afterAll(() => console.log('Inner afterAll'))
it('test', () => {
console.log('test')
})
})
})
// 执行顺序:
// Outer beforeAll
// Inner beforeAll
// Outer beforeEach
// Inner beforeEach
// test
// Inner afterEach
// Outer afterEach
// Inner afterAll
// Outer afterAll
异步钩子 #
typescript
import { describe, beforeAll, afterAll, it } from 'vitest'
describe('Async Hooks', () => {
let db: Database
// 异步 setup
beforeAll(async () => {
db = await connectDatabase()
await db.migrate()
})
// 异步 teardown
afterAll(async () => {
await db.cleanup()
await db.disconnect()
})
it('should work with database', async () => {
const result = await db.query('SELECT 1')
expect(result).toBeDefined()
})
})
测试跳过与聚焦 #
跳过测试 #
typescript
import { describe, it, expect } from 'vitest'
describe('My Suite', () => {
it('normal test', () => {
expect(1).toBe(1)
})
// 跳过单个测试
it.skip('skipped test', () => {
// 这个测试不会运行
expect(1).toBe(2)
})
// 跳过整个套件
describe.skip('Skipped Suite', () => {
it('test 1', () => {
// 不会运行
})
it('test 2', () => {
// 不会运行
})
})
})
聚焦测试 #
typescript
import { describe, it, expect } from 'vitest'
describe('My Suite', () => {
it('normal test', () => {
// 这个测试不会运行
})
// 只运行这个测试
it.only('focused test', () => {
expect(1).toBe(1)
})
// 只运行这个套件中的测试
describe.only('Focused Suite', () => {
it('test 1', () => {
// 会运行
})
it('test 2', () => {
// 会运行
})
})
})
条件跳过 #
typescript
import { describe, it, expect } from 'vitest'
describe('Conditional Tests', () => {
// 根据条件跳过
it.skipIf(process.env.NODE_ENV === 'production')('dev only test', () => {
// 生产环境跳过
})
// 根据条件运行
it.runIf(process.env.CI === 'true')('CI only test', () => {
// 只在 CI 环境运行
})
// 根据条件跳过(使用函数)
const isMacOS = process.platform === 'darwin'
it.skipIf(isMacOS)('non-macOS test', () => {
// macOS 跳过
})
})
待办测试 #
typescript
import { describe, it } from 'vitest'
describe('Todo Tests', () => {
// 标记为待办
it.todo('should implement feature X')
it.todo('should handle edge case Y', () => {
// 可以有实现,但标记为待办
// 运行时会显示为待办状态
})
})
测试并发 #
并行测试 #
typescript
import { describe, it, expect } from 'vitest'
// 默认并行执行
describe('Parallel Tests', () => {
it('test 1', async () => {
await new Promise(r => setTimeout(r, 100))
console.log('test 1 done')
})
it('test 2', async () => {
await new Promise(r => setTimeout(r, 100))
console.log('test 2 done')
})
it('test 3', async () => {
await new Promise(r => setTimeout(r, 100))
console.log('test 3 done')
})
})
串行测试 #
typescript
import { describe, it, expect } from 'vitest'
// 强制串行执行
describe.serial('Serial Tests', () => {
it('test 1', async () => {
// 等待 test 1 完成
})
it('test 2', async () => {
// test 1 完成后才执行
})
it('test 3', async () => {
// test 2 完成后才执行
})
})
并发控制 #
typescript
import { describe, it, expect } from 'vitest'
// 指定并发数
describe.concurrent('Concurrent Tests', () => {
it('test 1', async () => {
// 并行执行
})
it('test 2', async () => {
// 并行执行
})
})
// 单个测试并发
it.concurrent('concurrent test', async () => {
// 并行执行
})
测试重复 #
重复运行测试 #
typescript
import { describe, it, expect } from 'vitest'
describe('Repeat Tests', () => {
// 重复运行 5 次
it('flaky test', () => {
const random = Math.random()
expect(random).toBeLessThan(1)
}, { repeats: 5 })
// 重复直到失败
it('should eventually fail', () => {
// ...
}, { repeats: 10, retry: 2 })
})
测试重试 #
失败重试 #
typescript
import { describe, it, expect } from 'vitest'
describe('Retry Tests', () => {
// 失败后重试 3 次
it('flaky network test', async () => {
const response = await fetch('/api/data')
expect(response.ok).toBe(true)
}, { retry: 3 })
// 结合重复和重试
it('very flaky test', async () => {
// ...
}, { repeats: 5, retry: 2 })
})
测试超时 #
设置超时 #
typescript
import { describe, it, expect } from 'vitest'
describe('Timeout Tests', () => {
// 默认 5000ms
it('normal test', () => {
// ...
})
// 自定义超时时间(毫秒)
it('long running test', async () => {
await new Promise(r => setTimeout(r, 3000))
}, 5000) // 5 秒超时
// 钩子超时
beforeEach(async () => {
// ...
}, 10000) // 10 秒超时
})
全局超时配置 #
typescript
// vitest.config.ts
import { defineConfig } from 'vitest'
export default defineConfig({
test: {
testTimeout: 10000, // 测试超时
hookTimeout: 10000, // 钩子超时
},
})
测试上下文 #
使用测试上下文 #
typescript
import { describe, it, expect, beforeEach } from 'vitest'
describe('Test Context', () => {
// 使用上下文传递数据
beforeEach((context) => {
context.userId = 1
context.userName = 'test'
})
it('should have context', ({ userId, userName }) => {
expect(userId).toBe(1)
expect(userName).toBe('test')
})
it('can modify context', (context) => {
context.newData = 'added'
expect(context.newData).toBe('added')
})
})
内置上下文属性 #
typescript
import { describe, it, expect } from 'vitest'
describe('Built-in Context', () => {
it('access context properties', (context) => {
// 任务信息
console.log(context.task.name) // 测试名称
console.log(context.task.suite) // 所属套件
// 元数据存储
context.meta.customData = 'value'
})
})
参数化测试 #
使用 each 方法 #
typescript
import { describe, it, expect } from 'vitest'
describe('Parameterized Tests', () => {
// 基本参数化
it.each([
[1, 1, 2],
[1, 2, 3],
[2, 2, 4],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(a + b).toBe(expected)
})
// 使用对象
it.each([
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 3, expected: 6 },
])('double($input) = $expected', ({ input, expected }) => {
expect(input * 2).toBe(expected)
})
})
使用 for 循环 #
typescript
import { describe, it, expect } from 'vitest'
describe('Loop Tests', () => {
const testCases = [
{ input: 'hello', expected: 5 },
{ input: 'world', expected: 5 },
{ input: '', expected: 0 },
]
for (const { input, expected } of testCases) {
it(`length of "${input}" should be ${expected}`, () => {
expect(input.length).toBe(expected)
})
}
})
使用 describe.each #
typescript
import { describe, it, expect } from 'vitest'
describe.each([
{ a: 1, b: 1, expected: 2 },
{ a: 1, b: 2, expected: 3 },
{ a: 2, b: 3, expected: 5 },
])('describe with a=$a, b=$b', ({ a, b, expected }) => {
it(`should return ${expected}`, () => {
expect(a + b).toBe(expected)
})
it(`should be greater than 0`, () => {
expect(a + b).toBeGreaterThan(0)
})
})
测试分组 #
使用 describe 进行分组 #
typescript
import { describe, it, expect } from 'vitest'
describe('String Utils', () => {
describe('capitalize', () => {
it('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello')
})
it('should handle empty string', () => {
expect(capitalize('')).toBe('')
})
})
describe('reverse', () => {
it('should reverse string', () => {
expect(reverse('hello')).toBe('olleh')
})
it('should handle single character', () => {
expect(reverse('a')).toBe('a')
})
})
})
测试隔离 #
使用 beforeEach 重置状态 #
typescript
import { describe, it, expect, beforeEach } from 'vitest'
describe('Test Isolation', () => {
let counter = 0
beforeEach(() => {
counter = 0 // 每个测试前重置
})
it('test 1', () => {
counter++
expect(counter).toBe(1)
})
it('test 2', () => {
counter++
expect(counter).toBe(1) // 仍然是 1,因为已重置
})
})
使用作用域隔离 #
typescript
import { describe, it, expect, beforeEach } from 'vitest'
describe('Outer', () => {
let outerVar = 'outer'
beforeEach(() => {
outerVar = 'outer'
})
describe('Inner', () => {
let innerVar = 'inner'
beforeEach(() => {
innerVar = 'inner'
})
it('can access both variables', () => {
expect(outerVar).toBe('outer')
expect(innerVar).toBe('inner')
})
})
})
测试辅助函数 #
自定义测试工具 #
typescript
// test/utils.ts
import { expect } from 'vitest'
export function createTestUser(overrides = {}) {
return {
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides,
}
}
export function expectUserToBeValid(user: any) {
expect(user).toHaveProperty('id')
expect(user).toHaveProperty('name')
expect(user).toHaveProperty('email')
expect(user.email).toMatch(/@/)
}
typescript
// test/user.test.ts
import { describe, it } from 'vitest'
import { createTestUser, expectUserToBeValid } from './utils'
describe('User', () => {
it('should be valid', () => {
const user = createTestUser()
expectUserToBeValid(user)
})
it('should accept overrides', () => {
const user = createTestUser({ name: 'Custom Name' })
expect(user.name).toBe('Custom Name')
})
})
测试文件组织 #
推荐的目录结构 #
text
project/
├── src/
│ ├── utils/
│ │ ├── sum.ts
│ │ └── sum.test.ts # 测试文件与源文件同目录
│ └── components/
│ ├── Button.tsx
│ └── Button.test.tsx
├── test/ # 集成测试目录
│ ├── setup.ts
│ └── integration/
│ └── api.test.ts
└── vitest.config.ts
测试文件命名规范 #
text
# 推荐命名方式
sum.test.ts # 单元测试
sum.spec.ts # 规格测试
Button.test.tsx # 组件测试
api.integration.test.ts # 集成测试
下一步 #
现在你已经学会了如何编写测试,接下来学习 断言 了解更多断言方法!
最后更新:2026-03-28