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