Skip to content

Mock、Spy 与测试隔离:练习答案 ​

重试实现与有限次 mock ​

ts
export async function retry<T>(operation: () => Promise<T>, attempts = 2) {
  let lastError: unknown
  for (let i = 0; i < attempts; i++) {
    try { return await operation() }
    catch (error) { lastError = error }
  }
  throw lastError
}
ts
const operation = vi.fn()
  .mockRejectedValueOnce(new Error('offline'))
  .mockResolvedValueOnce('ok')
await expect(retry(operation)).resolves.toBe('ok')
expect(operation).toHaveBeenCalledTimes(2)

mockRejectedValueOnce 只影响下一次调用,正好表达“第一次失败、第二次成功”;用永久 mockRejectedValue 会让重试永远失败。

清理时间 ​

ts
afterEach(() => {
  vi.useRealTimers()
  vi.restoreAllMocks()
  vi.clearAllMocks()
})

useRealTimers 恢复宿主时钟,restoreAllMocks 恢复 spy 的原方法,clearAllMocks 清调用记录。若配置中打开 restoreMocks: true,仍需在使用 fake timer 的测试后恢复真实 timer;mock 配置不会代替时间清理。

模块替换的隔离 ​

如果模块可改,优先把 send 作为参数注入。必须替换模块时:

ts
it('每个导入看到自己的 mock', async () => {
  vi.resetModules()
  vi.doMock('./transport', () => ({ send: vi.fn().mockResolvedValue({ status: 500 }) }))
  const { publishReport } = await import('./report')
  await expect(publishReport({ title: '周报' })).rejects.toThrow()
  vi.doUnmock('./transport')
})

doMock 不提升,动态 import 发生在 mock 之后;resetModules 清模块缓存,否则前一个用例导入的 report 仍持有旧依赖。若使用静态 import,mock 发生太晚,整文件执行时就会与单测执行顺序产生差异。

可运行核对:先只运行重试测试,再运行整个文件;两次都应得到相同调用次数。故意删除 afterEach 或 resetModules 后,必须能看到污染而不是仍然“通过”。

用心记录每一步成长