Chai 教程

1. 核心知识点讲解

1.1 Chai 简介

Chai 是一个功能丰富的 JavaScript 断言库,可以与任何 JavaScript 测试框架集成。它提供了三种不同风格的断言语法:expect、should 和 assert,满足不同开发者的偏好。

1.2 安装和配置

安装 Chai:

npm install --save-dev chai

基本使用:

// 引入 Chai
const chai = require('chai');

// 选择断言风格
const expect = chai.expect; // expect 风格
const assert = chai.assert; // assert 风格
chai.should(); // should 风格

1.3 三种断言风格

1.3.1 Expect 风格

Expect 风格使用链式调用,语法直观易读:

const expect = require('chai').expect;

expect(1 + 1).to.equal(2);
expect({ a: 1 }).to.deep.equal({ a: 1 });
expect(true).to.be.true;
expect(false).to.be.false;
expect(null).to.be.null;
expect(undefined).to.be.undefined;
expect('hello').to.be.a('string');
expect([1, 2, 3]).to.be.an('array');
expect([1, 2, 3]).to.include(2);
expect('hello').to.include('ell');
expect(() => { throw new Error('错误'); }).to.throw('错误');

1.3.2 Should 风格

Should 风格通过扩展 Object.prototype 来提供断言方法:

const should = require('chai').should();

(1 + 1).should.equal(2);
({ a: 1 }).should.deep.equal({ a: 1 });
true.should.be.true;
false.should.be.false;
null.should.be.null;
// undefined.should.be.undefined; // 注意:undefined 不能使用 should 风格
'hello'.should.be.a('string');
[1, 2, 3].should.be.an('array');
[1, 2, 3].should.include(2);
'hello'.should.include('ell');
(() => { throw new Error('错误'); }).should.throw('错误');

1.3.3 Assert 风格

Assert 风格使用传统的函数调用语法:

const assert = require('chai').assert;

assert.equal(1 + 1, 2);
assert.deepEqual({ a: 1 }, { a: 1 });
assert.isTrue(true);
assert.isFalse(false);
assert.isNull(null);
assert.isUndefined(undefined);
assert.isString('hello');
assert.isArray([1, 2, 3]);
assert.include([1, 2, 3], 2);
assert.include('hello', 'ell');
assert.throws(() => { throw new Error('错误'); }, '错误');

1.4 常用断言方法

1.4.1 相等性断言

// Expect 风格
expect(actual).to.equal(expected); // 严格相等
expect(actual).to.eql(expected); // 深度相等
expect(actual).to.deep.equal(expected); // 深度相等

// Should 风格
actual.should.equal(expected);
actual.should.eql(expected);
actual.should.deep.equal(expected);

// Assert 风格
assert.equal(actual, expected);
assert.deepEqual(actual, expected);
assert.strictEqual(actual, expected); // 严格相等(===)
assert.notStrictEqual(actual, expected); // 不严格相等(!==)

1.4.2 类型断言

// Expect 风格
expect(value).to.be.a('string');
expect(value).to.be.an('array');
expect(value).to.be.an('object');
expect(value).to.be.a('function');

// Should 风格
value.should.be.a('string');
value.should.be.an('array');

// Assert 风格
assert.typeOf(value, 'string');
assert.isString(value);
assert.isArray(value);
assert.isObject(value);
assert.isFunction(value);

1.4.3 存在性断言

// Expect 风格
expect(value).to.exist;
expect(value).to.not.exist;

// Should 风格
value.should.exist;
value.should.not.exist;

// Assert 风格
assert.exists(value);
assert.notExists(value);

1.4.4 布尔值断言

// Expect 风格
expect(value).to.be.true;
expect(value).to.be.false;

// Should 风格
value.should.be.true;
value.should.be.false;

// Assert 风格
assert.isTrue(value);
assert.isFalse(value);

1.4.5 包含断言

// Expect 风格
expect(array).to.include(value);
expect(string).to.include(substring);
expect(object).to.include.key(key);
expect(object).to.include.keys(keys);

// Should 风格
array.should.include(value);
string.should.include(substring);
object.should.include.key(key);
object.should.include.keys(keys);

// Assert 风格
assert.include(array, value);
assert.include(string, substring);
assert.hasAnyKeys(object, keys);
assert.hasAllKeys(object, keys);

1.4.6 错误断言

// Expect 风格
expect(fn).to.throw();
expect(fn).to.throw(error);
expect(fn).to.throw(/regex/);

// Should 风格
fn.should.throw();
fn.should.throw(error);
fn.should.throw(/regex/);

// Assert 风格
assert.throws(fn);
assert.throws(fn, error);
assert.throws(fn, /regex/);

1.5 链式断言

Chai 支持链式调用,使断言更加直观:

// Expect 风格
expect(1 + 1)
  .to.be.a('number')
  .and.to.equal(2)
  .and.to.be.greaterThan(1)
  .and.to.be.lessThan(3);

// Should 风格
(1 + 1)
  .should.be.a('number')
  .and.equal(2)
  .and.be.greaterThan(1)
  .and.be.lessThan(3);

1.6 否定断言

使用 .not 来否定断言:

// Expect 风格
expect(1 + 1).to.not.equal(3);
expect([1, 2, 3]).to.not.include(4);

// Should 风格
(1 + 1).should.not.equal(3);
[1, 2, 3].should.not.include(4);

// Assert 风格
assert.notEqual(1 + 1, 3);
assert.notInclude([1, 2, 3], 4);

1.7 深度断言

使用 .deep 来进行深度比较:

// Expect 风格
expect({ a: { b: 1 } }).to.deep.equal({ a: { b: 1 } });

// Should 风格
({ a: { b: 1 } }).should.deep.equal({ a: { b: 1 } });

// Assert 风格
assert.deepEqual({ a: { b: 1 } }, { a: { b: 1 } });

1.8 自定义断言

Chai 允许你创建自定义断言:

const chai = require('chai');

// 添加自定义断言
chai.Assertion.addMethod('even', function() {
  const obj = this._obj;
  new chai.Assertion(obj).to.be.a('number');
  this.assert(
    obj % 2 === 0,
    'expected #{this} to be even',
    'expected #{this} to not be even',
    obj
  );
});

// 使用自定义断言
const expect = chai.expect;
expect(2).to.be.even;
expect(3).to.not.be.even;

2. 实用案例分析

2.1 基本断言案例

场景: 测试一个简单的计算器函数

实现:

// src/calculator.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('除数不能为零');
  }
  return a / b;
}

module.exports = {
  add,
  subtract,
  multiply,
  divide
};

// test/calculator.test.js
const { expect } = require('chai');
const { add, subtract, multiply, divide } = require('../src/calculator');

describe('计算器测试', function() {
  describe('加法测试', function() {
    it('1 + 2 应该等于 3', function() {
      expect(add(1, 2)).to.equal(3);
    });

    it('加法结果应该是数字类型', function() {
      expect(add(1, 2)).to.be.a('number');
    });

    it('负数相加应该正确', function() {
      expect(add(-1, -2)).to.equal(-3);
    });
  });

  describe('减法测试', function() {
    it('5 - 3 应该等于 2', function() {
      expect(subtract(5, 3)).to.equal(2);
    });

    it('减法结果应该是数字类型', function() {
      expect(subtract(5, 3)).to.be.a('number');
    });
  });

  describe('乘法测试', function() {
    it('2 * 3 应该等于 6', function() {
      expect(multiply(2, 3)).to.equal(6);
    });

    it('乘法结果应该是数字类型', function() {
      expect(multiply(2, 3)).to.be.a('number');
    });
  });

  describe('除法测试', function() {
    it('6 / 2 应该等于 3', function() {
      expect(divide(6, 2)).to.equal(3);
    });

    it('除法结果应该是数字类型', function() {
      expect(divide(6, 2)).to.be.a('number');
    });

    it('除数为零应该抛出错误', function() {
      expect(() => divide(1, 0)).to.throw('除数不能为零');
    });
  });
});

2.2 对象断言案例

场景: 测试一个用户对象

实现:

// src/user.js
function createUser(name, email, age) {
  return {
    id: Math.floor(Math.random() * 10000),
    name,
    email,
    age,
    createdAt: new Date()
  };
}

module.exports = { createUser };

// test/user.test.js
const { expect } = require('chai');
const { createUser } = require('../src/user');

describe('用户测试', function() {
  it('创建用户应该返回正确的对象结构', function() {
    const user = createUser('张三', 'zhangsan@example.com', 30);
    
    // 检查对象存在性
    expect(user).to.exist;
    expect(user).to.be.an('object');
    
    // 检查属性存在性
    expect(user).to.have.property('id');
    expect(user).to.have.property('name');
    expect(user).to.have.property('email');
    expect(user).to.have.property('age');
    expect(user).to.have.property('createdAt');
    
    // 检查属性值
    expect(user.name).to.equal('张三');
    expect(user.email).to.equal('zhangsan@example.com');
    expect(user.age).to.equal(30);
    
    // 检查属性类型
    expect(user.id).to.be.a('number');
    expect(user.name).to.be.a('string');
    expect(user.email).to.be.a('string');
    expect(user.age).to.be.a('number');
    expect(user.createdAt).to.be.a('date');
    
    // 检查邮箱格式
    expect(user.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });

  it('创建用户应该包含所有必要的键', function() {
    const user = createUser('李四', 'lisi@example.com', 25);
    expect(user).to.have.all.keys(['id', 'name', 'email', 'age', 'createdAt']);
  });
});

2.3 数组断言案例

场景: 测试一个数组处理函数

实现:

// src/arrayUtils.js
function filterEvenNumbers(numbers) {
  return numbers.filter(n => n % 2 === 0);
}

function sortNumbers(numbers) {
  return [...numbers].sort((a, b) => a - b);
}

function findMaxNumber(numbers) {
  if (numbers.length === 0) {
    return null;
  }
  return Math.max(...numbers);
}

module.exports = {
  filterEvenNumbers,
  sortNumbers,
  findMaxNumber
};

// test/arrayUtils.test.js
const { expect } = require('chai');
const { filterEvenNumbers, sortNumbers, findMaxNumber } = require('../src/arrayUtils');

describe('数组工具测试', function() {
  describe('过滤偶数测试', function() {
    it('应该返回数组中的所有偶数', function() {
      const numbers = [1, 2, 3, 4, 5, 6];
      const result = filterEvenNumbers(numbers);
      expect(result).to.deep.equal([2, 4, 6]);
    });

    it('结果应该是一个数组', function() {
      const numbers = [1, 2, 3];
      const result = filterEvenNumbers(numbers);
      expect(result).to.be.an('array');
    });

    it('空数组应该返回空数组', function() {
      const result = filterEvenNumbers([]);
      expect(result).to.be.an('array').that.is.empty;
    });
  });

  describe('排序测试', function() {
    it('应该对数组进行升序排序', function() {
      const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
      const result = sortNumbers(numbers);
      expect(result).to.deep.equal([1, 1, 2, 3, 4, 5, 6, 9]);
    });

    it('结果应该是一个数组', function() {
      const numbers = [3, 1, 4];
      const result = sortNumbers(numbers);
      expect(result).to.be.an('array');
    });

    it('不应该修改原数组', function() {
      const numbers = [3, 1, 4];
      const original = [...numbers];
      sortNumbers(numbers);
      expect(numbers).to.deep.equal(original);
    });
  });

  describe('查找最大值测试', function() {
    it('应该返回数组中的最大值', function() {
      const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
      const result = findMaxNumber(numbers);
      expect(result).to.equal(9);
    });

    it('空数组应该返回 null', function() {
      const result = findMaxNumber([]);
      expect(result).to.be.null;
    });

    it('单元素数组应该返回该元素', function() {
      const numbers = [42];
      const result = findMaxNumber(numbers);
      expect(result).to.equal(42);
    });
  });
});

3. 代码示例

3.1 三种断言风格对比

expect 风格:

const expect = require('chai').expect;

describe('Expect 风格测试', function() {
  it('基本断言', function() {
    expect(1 + 1).to.equal(2);
    expect('hello').to.be.a('string');
    expect([1, 2, 3]).to.include(2);
    expect({ a: 1 }).to.have.property('a');
  });

  it('链式调用', function() {
    expect(1 + 1)
      .to.be.a('number')
      .and.to.equal(2)
      .and.to.be.greaterThan(1);
  });

  it('否定断言', function() {
    expect(1 + 1).to.not.equal(3);
    expect([1, 2, 3]).to.not.include(4);
  });
});

should 风格:

const should = require('chai').should();

describe('Should 风格测试', function() {
  it('基本断言', function() {
    (1 + 1).should.equal(2);
    'hello'.should.be.a('string');
    [1, 2, 3].should.include(2);
    ({ a: 1 }).should.have.property('a');
  });

  it('链式调用', function() {
    (1 + 1)
      .should.be.a('number')
      .and.equal(2)
      .and.be.greaterThan(1);
  });

  it('否定断言', function() {
    (1 + 1).should.not.equal(3);
    [1, 2, 3].should.not.include(4);
  });
});

assert 风格:

const assert = require('chai').assert;

describe('Assert 风格测试', function() {
  it('基本断言', function() {
    assert.equal(1 + 1, 2);
    assert.typeOf('hello', 'string');
    assert.include([1, 2, 3], 2);
    assert.property({ a: 1 }, 'a');
  });

  it('否定断言', function() {
    assert.notEqual(1 + 1, 3);
    assert.notInclude([1, 2, 3], 4);
  });
});

3.2 高级断言示例

深度比较:

const { expect } = require('chai');

describe('深度比较测试', function() {
  it('深度比较对象', function() {
    const obj1 = { a: { b: { c: 1 } } };
    const obj2 = { a: { b: { c: 1 } } };
    expect(obj1).to.deep.equal(obj2);
  });

  it('深度比较数组', function() {
    const arr1 = [1, [2, [3]]];
    const arr2 = [1, [2, [3]]];
    expect(arr1).to.deep.equal(arr2);
  });
});

正则表达式匹配:

const { expect } = require('chai');

describe('正则表达式测试', function() {
  it('测试邮箱格式', function() {
    const email = 'user@example.com';
    expect(email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });

  it('测试电话号码格式', function() {
    const phone = '13812345678';
    expect(phone).to.match(/^1[3-9]\d{9}$/);
  });
});

错误断言:

const { expect } = require('chai');

describe('错误断言测试', function() {
  it('测试抛出错误', function() {
    function throwError() {
      throw new Error('测试错误');
    }
    expect(throwError).to.throw('测试错误');
  });

  it('测试抛出特定类型的错误', function() {
    function throwTypeError() {
      throw new TypeError('类型错误');
    }
    expect(throwTypeError).to.throw(TypeError);
  });
});

自定义断言:

const chai = require('chai');
const expect = chai.expect;

// 添加自定义断言
chai.Assertion.addMethod('positive', function() {
  const obj = this._obj;
  new chai.Assertion(obj).to.be.a('number');
  this.assert(
    obj > 0,
    'expected #{this} to be positive',
    'expected #{this} to not be positive',
    obj
  );
});

chai.Assertion.addMethod('negative', function() {
  const obj = this._obj;
  new chai.Assertion(obj).to.be.a('number');
  this.assert(
    obj < 0,
    'expected #{this} to be negative',
    'expected #{this} to not be negative',
    obj
  );
});

describe('自定义断言测试', function() {
  it('测试正数断言', function() {
    expect(5).to.be.positive;
    expect(-5).to.not.be.positive;
  });

  it('测试负数断言', function() {
    expect(-5).to.be.negative;
    expect(5).to.not.be.negative;
  });
});

3.3 与测试框架集成

与 Mocha 集成:

// test/integration.test.js
const { expect } = require('chai');
const { add, divide } = require('../src/calculator');

describe('与 Mocha 集成测试', function() {
  it('加法测试', function() {
    expect(add(1, 2)).to.equal(3);
  });

  it('除法测试', function() {
    expect(divide(6, 2)).to.equal(3);
  });
});

与 Jest 集成:

// src/__tests__/integration.test.js
const { expect } = require('chai');
const { add, divide } = require('../calculator');

describe('与 Jest 集成测试', () => {
  test('加法测试', () => {
    expect(add(1, 2)).to.equal(3);
  });

  test('除法测试', () => {
    expect(divide(6, 2)).to.equal(3);
  });
});

4. 总结

Chai 是一个功能丰富的 JavaScript 断言库,提供了三种不同风格的断言语法:expect、should 和 assert。本教程介绍了 Chai 的核心概念,包括:

  • 安装和配置
  • 三种断言风格的使用
  • 常用断言方法
  • 链式调用和否定断言
  • 深度比较和正则表达式匹配
  • 错误断言和自定义断言

通过学习这些概念,你可以根据自己的偏好选择合适的断言风格,并在测试中使用 Chai 提供的丰富断言方法,编写更加清晰、可读的测试代码。

Chai 的灵活性和扩展性使其成为 JavaScript 测试中的重要工具,无论是与 Mocha、Jest 还是其他测试框架集成,都能提供一致、直观的断言体验。

5. 进一步学习资源

希望本教程对你学习 Chai 有所帮助!

« 上一篇 Mocha 教程 下一篇 » Sinon 教程