Chai 中文文档 下载 App

异常捕获 .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 隔离副作用。

下一步