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