Vitest 异步测试 #
异步测试概述 #
JavaScript 是单线程语言,大量使用异步操作。测试异步代码需要特殊处理,Vitest 提供了多种方式来测试异步代码。
异步操作类型 #
text
┌─────────────────────────────────────────────────────────────┐
│ JavaScript 异步操作 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Promise │
│ fetch('/api/data').then(response => ...) │
│ │
│ 2. async/await │
│ const data = await fetchData() │
│ │
│ 3. 回调函数 │
│ setTimeout(() => ..., 1000) │
│ fs.readFile(path, (err, data) => ...) │
│ │
│ 4. 事件 │
│ element.addEventListener('click', handler) │
│ │
│ 5. 定时器 │
│ setTimeout / setInterval / requestAnimationFrame │
│ │
└─────────────────────────────────────────────────────────────┘
async/await 测试 #
基本用法 #
typescript
import { expect, test } from 'vitest'
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
test('async/await basic', async () => {
const user = await fetchUser(1)
expect(user).toHaveProperty('name')
})
测试异步函数 #
typescript
import { expect, test, vi } from 'vitest'
// 模拟异步函数
const fetchData = vi.fn(async (id: number) => {
return { id, data: 'mocked data' }
})
test('async function', async () => {
const result = await fetchData(1)
expect(result).toEqual({ id: 1, data: 'mocked data' })
expect(fetchData).toHaveBeenCalledWith(1)
})
并行异步操作 #
typescript
import { expect, test } from 'vitest'
test('parallel async operations', async () => {
// Promise.all 并行执行
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
])
expect(users).toBeInstanceOf(Array)
expect(posts).toBeInstanceOf(Array)
})
test('parallel with Promise.allSettled', async () => {
const results = await Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('failed')),
Promise.resolve(3),
])
expect(results[0]).toEqual({ status: 'fulfilled', value: 1 })
expect(results[1]).toEqual({ status: 'rejected', reason: expect.any(Error) })
expect(results[2]).toEqual({ status: 'fulfilled', value: 3 })
})
Promise 断言 #
resolves #
测试 Promise 成功的情况:
typescript
import { expect, test } from 'vitest'
test('resolves', async () => {
// 等待 Promise resolve
await expect(Promise.resolve(42)).resolves.toBe(42)
// 复杂对象
await expect(Promise.resolve({ id: 1, name: 'John' }))
.resolves.toEqual({ id: 1, name: 'John' })
// 链式断言
await expect(fetchUser(1))
.resolves
.toHaveProperty('name')
})
rejects #
测试 Promise 失败的情况:
typescript
import { expect, test } from 'vitest'
test('rejects', async () => {
// 等待 Promise reject
await expect(Promise.reject(new Error('failed')))
.rejects.toThrow('failed')
// 特定错误类型
await expect(Promise.reject(new TypeError('invalid type')))
.rejects.toThrow(TypeError)
// 自定义错误类
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message)
}
}
await expect(Promise.reject(new ValidationError('Invalid email', 'email')))
.rejects.toThrow(ValidationError)
})
resolves/rejects 与 async/await 对比 #
typescript
import { expect, test } from 'vitest'
test('two approaches comparison', async () => {
// 方式 1:使用 resolves
await expect(Promise.resolve(42)).resolves.toBe(42)
// 方式 2:使用 async/await
const value = await Promise.resolve(42)
expect(value).toBe(42)
// 两种方式等价,选择更清晰的方式
})
回调函数测试 #
使用回调参数 #
typescript
import { expect, test } from 'vitest'
function fetchData(callback: (data: string) => void) {
setTimeout(() => {
callback('data loaded')
}, 100)
}
test('callback with done', () => {
return new Promise<void>((done) => {
fetchData((data) => {
expect(data).toBe('data loaded')
done()
})
})
})
将回调转换为 Promise #
typescript
import { expect, test } from 'vitest'
function fetchData(callback: (data: string) => void) {
setTimeout(() => {
callback('data loaded')
}, 100)
}
// 包装为 Promise
function fetchDataAsync(): Promise<string> {
return new Promise((resolve) => {
fetchData(resolve)
})
}
test('callback converted to promise', async () => {
const data = await fetchDataAsync()
expect(data).toBe('data loaded')
})
使用 promisify #
typescript
import { expect, test } from 'vitest'
import { promisify } from 'util'
function fetchData(callback: (error: Error | null, data?: string) => void) {
setTimeout(() => {
callback(null, 'data loaded')
}, 100)
}
test('promisify callback', async () => {
const fetchDataAsync = promisify(fetchData)
const data = await fetchDataAsync()
expect(data).toBe('data loaded')
})
定时器测试 #
使用假定时器 #
typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
test('setTimeout', async () => {
const callback = vi.fn()
setTimeout(callback, 1000)
// 时间未到
expect(callback).not.toHaveBeenCalled()
// 快进时间
await vi.advanceTimersByTimeAsync(1000)
// 已调用
expect(callback).toHaveBeenCalled()
})
test('setInterval', async () => {
const callback = vi.fn()
setInterval(callback, 1000)
// 快进 3 秒
await vi.advanceTimersByTimeAsync(3000)
expect(callback).toHaveBeenCalledTimes(3)
})
异步定时器 #
typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
test('async timer', async () => {
async function delayedValue(value: number, delay: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(value), delay)
})
}
const promise = delayedValue(42, 1000)
// 快进时间
await vi.advanceTimersByTimeAsync(1000)
const result = await promise
expect(result).toBe(42)
})
运行所有定时器 #
typescript
import { vi, expect, test, beforeEach, afterEach } from 'vitest'
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
test('run all timers', async () => {
const callback = vi.fn()
setTimeout(() => callback('first'), 100)
setTimeout(() => callback('second'), 200)
setTimeout(() => callback('third'), 300)
// 运行所有定时器
await vi.runAllTimersAsync()
expect(callback).toHaveBeenCalledTimes(3)
})
事件测试 #
DOM 事件 #
typescript
import { expect, test, vi } from 'vitest'
import { render, fireEvent } from '@testing-library/vue'
import Button from './Button.vue'
test('DOM event', async () => {
const handleClick = vi.fn()
const { getByRole } = render(Button, {
props: { onClick: handleClick },
})
const button = getByRole('button')
await fireEvent.click(button)
expect(handleClick).toHaveBeenCalled()
})
自定义事件 #
typescript
import { expect, test, vi } from 'vitest'
class EventEmitter {
private listeners: Map<string, Function[]> = new Map()
on(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event)!.push(callback)
}
emit(event: string, data?: any) {
const callbacks = this.listeners.get(event) || []
callbacks.forEach(cb => cb(data))
}
}
test('custom event', () => {
const emitter = new EventEmitter()
const handler = vi.fn()
emitter.on('data', handler)
emitter.emit('data', { value: 42 })
expect(handler).toHaveBeenCalledWith({ value: 42 })
})
异步事件 #
typescript
import { expect, test, vi } from 'vitest'
function waitForEvent(emitter: EventEmitter, event: string): Promise<any> {
return new Promise((resolve) => {
emitter.on(event, resolve)
})
}
test('async event', async () => {
const emitter = new EventEmitter()
setTimeout(() => {
emitter.emit('data', { value: 42 })
}, 100)
const data = await waitForEvent(emitter, 'data')
expect(data).toEqual({ value: 42 })
})
错误处理 #
测试异步错误 #
typescript
import { expect, test } from 'vitest'
async function throwError(): Promise<never> {
throw new Error('Async error')
}
test('async error with rejects', async () => {
await expect(throwError()).rejects.toThrow('Async error')
})
test('async error with try/catch', async () => {
try {
await throwError()
expect.fail('Should have thrown')
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect((error as Error).message).toBe('Async error')
}
})
测试 Promise 链中的错误 #
typescript
import { expect, test } from 'vitest'
test('promise chain error', async () => {
const promise = Promise.resolve(1)
.then(value => {
if (value < 10) {
throw new Error('Value too small')
}
return value
})
await expect(promise).rejects.toThrow('Value too small')
})
超时处理 #
测试超时 #
typescript
import { expect, test } from 'vitest'
test('timeout test', async () => {
// 设置测试超时时间(毫秒)
const result = await slowOperation()
expect(result).toBeDefined()
}, 10000) // 10 秒超时
自定义超时错误 #
typescript
import { expect, test } from 'vitest'
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms)
}),
])
}
test('custom timeout', async () => {
const slowPromise = new Promise(resolve => {
setTimeout(() => resolve('done'), 5000)
})
await expect(withTimeout(slowPromise, 1000))
.rejects.toThrow('Timeout')
})
并发控制 #
测试并发限制 #
typescript
import { expect, test, vi } from 'vitest'
async function concurrentLimit<T>(
tasks: (() => Promise<T>)[],
limit: number
): Promise<T[]> {
const results: T[] = []
const executing: Promise<void>[] = []
for (const task of tasks) {
const promise = task().then(result => {
results.push(result)
})
executing.push(promise)
if (executing.length >= limit) {
await Promise.race(executing)
}
}
await Promise.all(executing)
return results
}
test('concurrent limit', async () => {
const tasks = Array(10).fill(null).map((_, i) =>
() => Promise.resolve(i)
)
const results = await concurrentLimit(tasks, 3)
expect(results).toHaveLength(10)
})
等待条件 #
等待函数 #
typescript
import { expect, test, vi } from 'vitest'
async function waitFor(
condition: () => boolean,
options: { timeout?: number; interval?: number } = {}
): Promise<void> {
const { timeout = 5000, interval = 50 } = options
const start = Date.now()
while (!condition()) {
if (Date.now() - start > timeout) {
throw new Error('Timeout waiting for condition')
}
await new Promise(resolve => setTimeout(resolve, interval))
}
}
test('wait for condition', async () => {
let ready = false
setTimeout(() => {
ready = true
}, 100)
await waitFor(() => ready, { timeout: 1000 })
expect(ready).toBe(true)
})
使用 vi.waitFor #
typescript
import { vi, expect, test } from 'vitest'
test('vi.waitFor', async () => {
const callback = vi.fn()
setTimeout(() => {
callback('data')
}, 100)
await vi.waitFor(() => {
expect(callback).toHaveBeenCalledWith('data')
})
})
使用 vi.waitUntil #
typescript
import { vi, expect, test } from 'vitest'
test('vi.waitUntil', async () => {
let value = 0
setTimeout(() => {
value = 42
}, 100)
await vi.waitUntil(() => value === 42)
expect(value).toBe(42)
})
异步测试最佳实践 #
1. 始终等待异步操作 #
typescript
import { expect, test } from 'vitest'
// 好的做法
test('proper async handling', async () => {
const data = await fetchData()
expect(data).toBeDefined()
})
// 不好的做法
test('missing await', async () => {
fetchData().then(data => {
expect(data).toBeDefined() // 可能不会执行
})
})
2. 使用合理的超时时间 #
typescript
import { expect, test } from 'vitest'
// 根据实际需要设置超时
test('with appropriate timeout', async () => {
const result = await slowOperation()
expect(result).toBeDefined()
}, 10000) // 给足够的超时时间
3. 清理异步副作用 #
typescript
import { expect, test, beforeEach, afterEach } from 'vitest'
let controller: AbortController
beforeEach(() => {
controller = new AbortController()
})
afterEach(() => {
controller.abort() // 取消未完成的请求
})
test('cleanup async', async () => {
const result = await fetchData({ signal: controller.signal })
expect(result).toBeDefined()
})
4. 使用 Promise 而非回调 #
typescript
import { expect, test } from 'vitest'
// 好的做法:使用 Promise
test('promise style', async () => {
const data = await fetchDataAsync()
expect(data).toBeDefined()
})
// 不好的做法:使用回调
test('callback style', () => {
return new Promise(done => {
fetchData(data => {
expect(data).toBeDefined()
done()
})
})
})
异步测试速查表 #
| 方法 | 说明 |
|---|---|
async/await |
测试异步函数 |
resolves |
测试 Promise 成功 |
rejects |
测试 Promise 失败 |
vi.useFakeTimers() |
使用假定时器 |
vi.advanceTimersByTimeAsync() |
异步快进时间 |
vi.runAllTimersAsync() |
异步运行所有定时器 |
vi.waitFor() |
等待条件满足 |
vi.waitUntil() |
等待直到条件为真 |
下一步 #
现在你已经掌握了异步测试,接下来学习 代码覆盖率 了解如何测量测试覆盖率!
最后更新:2026-03-28