异常捕获 .throw / Throw - 验证函数是否抛出预期错误
基本用法
异常断言有两种形式:
// expect 风格
expect(function () {
throw new Error('boom');
}).to.throw();
// assert 风格
assert.throw(function () {
throw new Error('boom');
});
关键:.throw 的参数必须是函数——Chai 需要调用这个函数来捕获异常。如果你直接传一个已执行表达式,异常会在断言之前就被抛出:
// ❌ 错误——函数被立即调用,断言还没来得及捕获
expect(throwSomething()).to.throw();
// ✅ 正确——传函数引用,Chai 替你调用
expect(throwSomething).to.throw();
// 或者用匿名函数
expect(() => throwSomething()).to.throw();
.throw() —— 抛出任意异常
expect(() => {
throw new Error();
}).to.throw(); // ✅ 任意异常都行
.not.throw() —— 不抛出异常:
expect(() => {
// 这个函数不应该抛异常
safeOperation();
}).to.not.throw();
.throw(ErrorType) —— 匹配异常类型
class ValidationError extends Error {}
class NotFoundError extends Error {}
expect(() => {
throw new ValidationError('invalid');
}).to.throw(ValidationError); // ✅ 精确类型
expect(() => {
throw new ValidationError('invalid');
}).to.throw(NotFoundError); // ❌ 类型不对
类型检查使用 instanceof,所以父类也能匹配子类异常:
expect(() => {
throw new ValidationError('invalid');
}).to.throw(Error); // ✅ ValidationError 继承自 Error
.throw(message) —— 匹配错误消息
参数为字符串时,匹配错误消息包含该字符串:
expect(() => {
throw new Error('invalid email format');
}).to.throw('invalid email'); // ✅ 消息包含 'invalid email'
expect(() => {
throw new Error('invalid email format');
}).to.throw('wrong'); // ❌ 不包含
配合正则
参数也可以是 RegExp,匹配错误消息:
expect(() => {
throw new Error('Error code: E404');
}).to.throw(/E\d+/); // ✅
.throw(ErrorType, message) —— 同时匹配类型和消息
两个参数可以组合:
expect(() => {
throw new TypeError('value must be a number');
}).to.throw(TypeError, 'value must be a number'); // ✅
参数顺序:类型 → 消息(字符串或正则)。
.Throw —— .throw 的大写别名
expect(() => throw new Error()).to.Throw();
// 和 .throw() 完全等价
历史原因,Chai 同时提供了 .throw 和 .Throw。
在链式中组合
expect(() => {
userService.findById(null);
}).to.throw(TypeError).that.has.property('message').that.includes('null');
先验证抛出 TypeError,再进一步检查错误对象的属性。
assert 风格的对应方法
assert.throw(fn); // 抛任意异常
assert.throw(fn, ErrorType); // 匹配类型
assert.throw(fn, 'error message'); // 匹配消息(包含)
assert.throw(fn, ErrorType, 'error message'); // 类型 + 消息
assert.throw(fn, /regex/); // 消息正则
assert.doesNotThrow(fn); // 不抛异常
assert.doesNotThrow(fn, ErrorType); // 不抛指定类型异常
常见陷阱
忘记传函数
这是最常见的错误:
// ❌ 直接执行——异常在断言之前抛出,测试崩溃
expect(JSON.parse('invalid json')).to.throw();
// ✅ 包一层函数
expect(() => JSON.parse('invalid json')).to.throw();
异步函数没 await 或 return
如果抛异常的是异步函数(返回 Promise),直接传函数不会等待——Chai 同步调用,Promise 的 reject 不会被捕获:
// ❌ async 函数抛的异常在 Promise 里,不是同步抛出
expect(async () => {
throw new Error('async error');
}).to.throw(); // 失败——函数本身正常返回了 Promise
// ✅ 用 chai-as-promised 插件的 .rejectedWith
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
await expect(async () => {
throw new Error('async error');
}).to.be.rejectedWith(Error);
详见 chai-as-promised。
验证错误类型而不是验证行为
异常断言应该验证正确的异常类型而不是断言"抛了某个异常":
// ❌ 太宽泛
expect(() => parse(input)).to.throw();
// ✅ 精确
expect(() => parse(input)).to.throw(ValidationError, 'email format invalid');
副作用被意外触发
// ❌ 这个函数除了抛异常还可能有副作用(写数据库、发请求)
expect(() => deleteUser(id)).to.throw();
// 即使抛出了权限异常,deleteUser 也可能在检查权限前已经执行了部分删除
确保被测函数在抛异常之前不执行任何不可逆操作,或用 mock 隔离副作用。
下一步
- 异步异常断言 → chai-as-promised
- 回调风格测试 → 异步测试
- 异常类型设计 → 考虑为你的代码定义自定义 Error 子类,让
.throw(MyError)更精确