Jest 如何测试异常处理并正确使用 toThrow 和 rejects?
异常测试不是为了证明代码“会报错”,而是确认它在错误输入、依赖失败和边界条件下报出正确的错,并且调用方能按预期处理。Jest 里同步异常主要用 toThrow,Promise 拒绝主要用 rejects,回调错误则要显式等待测试结束。最常见的误区是把函数先执行了,再把结果交给 expect,这样异常会在断言前就抛出。
同步异常怎么测?
toThrow 接收的是一个函数包装,而不是函数调用结果。可以匹配错误类型、完整消息、部分字符串或正则。
jsfunction divide(a, b) { if (b === 0) throw new RangeError('Division by zero') return a / b } test('除数为 0 时抛出 RangeError', () => { expect(() => divide(10, 0)).toThrow(RangeError) expect(() => divide(10, 0)).toThrow(/zero/) })
边界是不要过度依赖完整错误文案。文案经常为了用户体验调整,测试也会跟着碎。更稳定的断言是错误类型、错误 code,或关键短语。
Promise 拒绝怎么测?
异步函数返回 Promise 时,用 await expect(promise).rejects...。不要忘记 await 或 return,否则测试可能在 Promise 拒绝前就结束,形成假通过。
jsasync function fetchUser(api) { const res = await api.get('/user') if (!res.ok) throw new Error('API Error') return res.data } test('接口失败时 rejects', async () => { const api = { get: jest.fn().mockResolvedValue({ ok: false }) } await expect(fetchUser(api)).rejects.toThrow('API Error') })
如果你需要检查错误对象上的多个字段,可以用 try/catch,但要加 expect.assertions,避免没有抛错时测试仍然通过。
jstest('保留错误 code', async () => { expect.assertions(2) try { await readConfig('/missing') } catch (error) { expect(error).toBeInstanceOf(Error) expect(error.code).toBe('ENOENT') } })
回调错误和框架错误怎么测?
Node 风格回调可以用 done,但必须保证错误路径和成功路径都能结束测试。React 错误边界、日志上报这类场景还要临时 mock console.error,并在测试后恢复。
jstest('callback 返回错误', done => { loadFile('bad.txt', err => { expect(err).toBeInstanceOf(Error) expect(err.message).toContain('bad') done() }) })
追问
toThrow 为什么必须包一层函数?
因为 Jest 需要自己调用这段代码,才能捕获抛出的异常。写成 expect(divide(1, 0)).toThrow() 时,异常已经在 expect 执行前抛出,断言根本没机会运行。这个边界只针对同步异常;异步函数即使内部 throw,也会变成 rejected Promise。踩坑是把同步和异步写法混用,导致测试报错位置看起来很奇怪。
rejects.toThrow 和 try/catch 怎么取舍?
rejects.toThrow 简洁,适合只关心错误类型或消息的场景。try/catch 更啰嗦,但适合检查多个字段,比如 code、status、details。取舍标准是断言复杂度:一两个断言用 rejects,多字段检查用 try/catch。坑是 try/catch 里忘记 expect.assertions,当函数没有抛错时测试也可能悄悄通过。
错误消息应该精确匹配吗?
一般不建议完整精确匹配,除非这个消息本身就是公开 API。内部错误文案经常调整,精确匹配会让测试过于脆弱。更好的选择是匹配错误类型、错误码或关键关键词。边界是表单校验、CLI 输出、SDK 对外错误这类场景,用户依赖文案时就应该精确测试。
如何测试“没有抛错”?
同步函数可以写 expect(() => fn()).not.toThrow(),异步函数可以写 await expect(fn()).resolves.toEqual(...)。但不要滥用“没有抛错”作为唯一断言,因为函数可能什么也没做也能通过。取舍是它适合覆盖边界输入不崩溃,更关键的业务结果仍要单独断言。踩坑是只测 not.toThrow,漏掉返回值或副作用错误。
Mock 抛错时要注意什么?
同步依赖用 mockImplementation(() => { throw error }),异步依赖用 mockRejectedValue(error),不要混着用。错误对象最好带上真实业务会用到的字段,比如 code 或 response.status。边界是有些库抛出的不是 Error,而是普通对象,测试要和真实库行为一致。踩坑是 mock 得太理想,生产里的错误结构不同,catch 分支读取字段时再次报错。