
在 jest 单元测试中,直接对 `object.keys().map()` 等高阶函数使用 `async/await` 会导致断言失效——因为 `map` 不等待 promise 完成,所有 `expect(...).rejects.tothrow()` 调用实际未被 jest 捕获。需改用 `promise.all` 或自定义异步映射工具确保断言同步执行。
Jest 的 expect().rejects.toThrow() 必须在测试上下文中同步触发并等待其完成,否则 Jest 无法捕获异常、也无法判断断言是否通过。而原代码中:
Object.keys(Status).map(async (key) => {
if (Status[key] !== Status.PENDING_REVISION) {
mockAdsRepository.findOne = jest.fn(() =>
Promise.resolve({ ...adsDto, status: Status[key] }),
);
await expect(service.approve(1, 1)).rejects.toThrow(BadRequestException);
}
});Array.prototype.map() 会立即返回一个包含多个 pending Promise 的数组,但 Jest 完全忽略该数组——既不 await 它,也不检查其中任何 Promise 的结果。这导致:
- 所有 expect(...).rejects.toThrow() 实际未被执行(或在测试结束之后才 resolve);
- 测试“静默通过”,即使某些状态本应成功(如 PENDING_REVISION)却未被验证;
- 无法定位具体哪个枚举值触发了意外行为。
✅ 正确做法:显式等待所有异步断言完成。推荐两种可靠方案:
方案一:使用 Promise.all() + for...of(简洁推荐)
it('should block approval of ads if status is not in pending revision', async () => {
const invalidStatuses = Object.values(Status).filter(
status => status !== Status.PENDING_REVISION,
);
// 逐个测试,全部 await
await Promise.all(
invalidStatuses.map(async (status) => {
mockAdsRepository.findOne = jest.fn(() =>
Promise.resolve({ ...adsDto, status }),
);
await expect(service.approve(1, 1)).rejects.toThrow(BadRequestException);
}),
);
});✅ 优势:无需额外工具函数;语义清晰;Promise.all 确保所有断言并发执行且全部完成后再退出测试。
方案二:封装可复用的 mapAsync 工具(适合复杂场景)
如答案中所提,可创建类型安全的异步映射函数:
// utils/test-utils.ts export async function mapAsync( elements: readonly T[], mapAction: (item: T) => Promise , ): Promise { const results: R[] = []; for (const element of elements) { results.push(await mapAction(element)); } return results; }
在测试中使用:
import { mapAsync } from './utils/test-utils';
it('should block approval of ads if status is not in pending revision', async () => {
const invalidStatuses = Object.values(Status).filter(
status => status !== Status.PENDING_REVISION,
);
await mapAsync(invalidStatuses, async (status) => {
mockAdsRepository.findOne = jest.fn(() =>
Promise.resolve({ ...adsDto, status }),
);
await expect(service.approve(1, 1)).rejects.toThrow(BadRequestException);
});
});⚠️ 注意事项:
- ❌ 避免 forEach(async () => {}) —— 同样存在未等待问题;
- ❌ 不要省略 await 在 expect(...).rejects.toThrow() 前,否则 Jest 会报 Error: expect(received).rejects.toThrow() cannot be called on a non-promise;
- ✅ 使用 Object.values(Status) 而非 Object.keys(Status),避免将数字键(如 0, 1)误作枚举值(TypeScript 枚举默认双向映射,keys() 会返回字符串键和数字索引);
- ✅ 若需测试「唯一允许通过的状态」,建议单独补一个正向用例:
it('should allow approval only when status is PENDING_REVISION', async () => { mockAdsRepository.findOne = jest.fn(() => Promise.resolve({ ...adsDto, status: Status.PENDING_REVISION }), ); await expect(service.approve(1, 1)).resolves.not.toThrow(); // 或具体返回值断言 });
总结:Jest 测试中所有异步断言必须被显式 await 并纳入测试生命周期。循环内断言不是语法限制,而是执行时序问题——用 Promise.all 或顺序 for...of + await 即可彻底解决,保障测试健壮性与可维护性。










