js异步测试关键在于控制执行时机、捕获异常和断言中间状态。需处理未捕获promise错误、竞态条件、超时取消逻辑、分阶段状态验证,并正确使用await waitfor配合显式timeout。

JS 异步代码测试排错,关键不在“等它跑完”,而在于“控制它怎么跑、何时跑、跑崩了有没有兜住”。很多测试失败不是逻辑错,是时机没控好、状态没断言全、错误没捕获到。
一、Promise 未捕获异常是最隐蔽的“静默失败”
异步错误若没被 .catch() 或 try/catch 捕获,会变成 unhandledRejection,Jest 默认不报错(但会警告),导致测试看似通过,实际业务逻辑已中断。
- 在 Jest 中启用严格模式:jest.unstable_mockModule 或配置
unhandledRejection: 'error',让未捕获 Promise reject 直接使测试失败 - 每个返回 Promise 的函数调用后,显式加 .catch(expect.fail) 或 await expect(...).rejects.toThrow(),堵住漏网之鱼
- 避免在测试中写
someAsyncFn().then(...)却不接 catch —— 这等于把错误扔进黑洞
二、竞态条件(Race Condition)测试必须“人为制造先后”
比如搜索建议组件:用户快速输入 “a” → “ab” → “abc”,三个请求并发发出,但“abc”的响应可能先到,“a”的后到。若没做防抖或取消,旧结果会覆盖新结果。
- 用 mock 实现“慢请求先发、快请求后发”:第一个 fetch 返回
new Promise(r => setTimeout(() => r({data: 'a'}), 300)),第二个返回Promise.resolve({data: 'abc'}) - 断言最终 state 或 DOM 文本只包含 “abc”,且“a”的回调函数 一次都没执行(检查
mockFn.mock.calls.length === 0) - 对 React 组件,配合
act(async () => {...})+waitFor确保渲染完成后再断言,避免“快照拍在中间态”
三、超时与取消逻辑不能靠“等真实网络”,得 mock 控制
真实 fetch 超时不可控,测试中必须用可控延迟模拟边界行为。
- 测试超时分支:给 axios/fetch 封装层传入
timeout: 10,mock 的 Promise 在 15ms 后 resolve —— 应触发超时错误 - 测试 AbortController 取消:创建 controller,在发起请求后立即
controller.abort(),验证请求函数是否提前退出、是否没调用setState或resolve - 检查清理动作:如定时器是否
clearTimeout、事件监听器是否removeEventListener、loading 状态是否重置为 false
四、生命周期状态断言要分阶段,不能只看“最终结果”
一个 loading → data → error 的异步流程,中间状态变化本身就是契约。只测最终值,等于放弃对交互体验的保障。
- 初始断言:
expect(screen.getByRole('button')).not.toBeDisabled()、expect(state.loading).toBe(false) - 发起后立即断言:
await waitFor(() => expect(state.loading).toBe(true))(注意:这是同步更新,应瞬间生效) - 成功后断言:
await waitFor(() => expect(state.data).toBeDefined()),同时确认loading === false且error === null - 失败后同理:确保
error有值、data为空、loading已关闭
五、async/await 测试陷阱:别忘了 await waitFor 和 timeout 设置
Jest 默认 5 秒超时,但边界测试常需更短时间暴露问题;用 done 回调则堆栈难读、易遗漏。
- 所有异步断言优先用
await waitFor(() => expect(...).toBe(...)),语义清晰、失败时定位准 - 为每个
waitFor显式设{ timeout: 100 },防止因 mock 延迟过长导致误判为“卡死” - 禁用
done:它要求手动调用,一旦忘记或写错位置,测试就挂起(Jest 会超时报错,但看不出哪一行漏了)











