Chai の使い方 — JavaScript/TypeScript テストのためのアサーションライブラリ
一言でいうと
Chai は、Node.js とブラウザの両方で動作する BDD/TDD 対応のアサーションライブラリです。expect・should・assert の3つのスタイルを提供し、Mocha・Vitest など任意のテストフレームワークと組み合わせて使えます。
どんな時に使う?
- テストコードで自然言語に近い読みやすいアサーションを書きたい時 —
expect(result).to.be.an('array').that.has.lengthOf(3)のようなチェーン記法で、テストの意図が一目で伝わります - Node.js 組み込みの
assertでは表現力が足りない時 — 深い等値比較、プロパティの存在チェック、例外の検証など、豊富なアサーションメソッドが揃っています - テストフレームワークに依存しないアサーション基盤が欲しい時 — Mocha、Vitest、その他どのテストランナーとも組み合わせ可能です
インストール
# npm
npm install --save-dev chai
# yarn
yarn add --dev chai
# pnpm
pnpm add -D chai
注意: Chai v5 以降は ESM only です。v6.x(現行バージョン)では Node.js 18 以上が必要です。
基本的な使い方
最もよく使われる expect スタイルの例です。
// sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
// sum.test.ts(Mocha + Chai の例)
import { expect } from 'chai';
import { sum } from './sum.js';
describe('sum', () => {
it('2つの数値を加算する', () => {
const result = sum(1, 2);
expect(result).to.equal(3);
});
it('負の数を扱える', () => {
expect(sum(-1, -2)).to.equal(-3);
});
it('数値を返す', () => {
expect(sum(0, 0)).to.be.a('number');
});
});
3つのアサーションスタイル
Chai は用途や好みに応じて3つのスタイルを選択できます。
import { assert, expect, should } from 'chai';
const value = 42;
// Assert スタイル(TDD 寄り)
assert.equal(value, 42);
assert.typeOf(value, 'number');
// Expect スタイル(BDD 寄り・最も人気)
expect(value).to.equal(42);
expect(value).to.be.a('number');
// Should スタイル(BDD 寄り・Object.prototype を拡張)
should();
value.should.equal(42);
value.should.be.a('number');
Tips:
shouldスタイルはObject.prototypeを変更するため、nullやundefinedに対して使えません。特別な理由がなければexpectスタイルを推奨します。
よく使うAPI
1. 等値比較 — equal / deep.equal
import { expect } from 'chai';
// 厳密等値(===)
expect(1 + 1).to.equal(2);
expect('hello').to.equal('hello');
// 深い等値比較(オブジェクト・配列の中身を再帰的に比較)
expect({ a: 1, b: { c: 2 } }).to.deep.equal({ a: 1, b: { c: 2 } });
expect([1, 2, 3]).to.deep.equal([1, 2, 3]);
// 否定
expect(1).to.not.equal(2);
2. 型チェック — a / an / instanceof
import { expect } from 'chai';
expect('hello').to.be.a('string');
expect(42).to.be.a('number');
expect(true).to.be.a('boolean');
expect([1, 2]).to.be.an('array');
expect(null).to.be.a('null');
expect(undefined).to.be.an('undefined');
expect({ a: 1 }).to.be.an('object');
// クラスのインスタンスチェック
class User {
constructor(public name: string) {}
}
const user = new User('Alice');
expect(user).to.be.an.instanceof(User);
3. プロパティ検証 — property / include / keys
import { expect } from 'chai';
const user = { name: 'Alice', age: 30, address: { city: 'Tokyo' } };
// プロパティの存在と値
expect(user).to.have.property('name');
expect(user).to.have.property('name', 'Alice');
// ネストしたプロパティ(deep.property は nested で)
expect(user).to.have.nested.property('address.city', 'Tokyo');
// オブジェクトの部分一致
expect(user).to.include({ name: 'Alice', age: 30 });
// キーの検証
expect(user).to.have.all.keys('name', 'age', 'address');
expect(user).to.have.any.keys('name', 'email');
// 配列の要素を含むか
expect([1, 2, 3]).to.include(2);
expect('hello world').to.include('world');
// 配列内のオブジェクト部分一致
expect([{ a: 1 }, { b: 2 }]).to.deep.include({ a: 1 });
4. 例外の検証 — throw
import { expect } from 'chai';
function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// 例外がスローされることを検証(関数をラップして渡す)
expect(() => divide(1, 0)).to.throw();
expect(() => divide(1, 0)).to.throw(Error);
expect(() => divide(1, 0)).to.throw('Division by zero');
expect(() => divide(1, 0)).to.throw(Error, /Division/);
// 例外がスローされないことを検証
expect(() => divide(4, 2)).to.not.throw();
5. 数値・真偽値・null 系の検証
import { expect } from 'chai';
// 数値の比較
expect(10).to.be.above(5); // >
expect(10).to.be.below(20); // <
expect(10).to.be.at.least(10); // >=
expect(10).to.be.at.most(10); // <=
expect(5.5).to.be.closeTo(5, 0.6); // 許容誤差内
// 真偽値・存在チェック
expect(true).to.be.true;
expect(false).to.be.false;
expect(null).to.be.null;
expect(undefined).to.be.undefined;
expect('hello').to.exist; // null/undefined でない
expect('').to.be.empty; // 空文字列・空配列・空オブジェクト
expect([]).to.be.empty;
expect({}).to.be.empty;
// 配列の長さ
expect([1, 2, 3]).to.have.lengthOf(3);
// 正規表現マッチ
expect('hello world').to.match(/^hello/);
類似パッケージとの比較
| 特徴 | Chai | Node.js assert | Jest expect | Vitest expect |
|---|---|---|---|---|
BDD スタイル(expect/should) | ✅ | ❌ | ✅ | ✅ |
TDD スタイル(assert) | ✅ | ✅ | ❌ | ❌ |
| テストフレームワーク非依存 | ✅ | ✅ | ❌(Jest 専用) | ❌(Vitest 専用) |
| プラグインエコシステム | ✅ 豊富 | ❌ | ✅ | ✅(Chai 互換) |
| ブラウザ対応 | ✅ | ❌ | ❌ | ❌ |
| 追加インストール | 必要 | 不要(組み込み) | 不要(Jest 同梱) | 不要(Vitest 同梱) |
| ESM 対応 | ✅(v5 以降 ESM only) | ✅ | ✅ | ✅ |
補足: Vitest は内部的に Chai をベースにしており、
expectの API がほぼ互換です。Vitest を使っている場合は Chai を別途インストールする必要はありません。
注意点・Tips
1. v5 以降は ESM only
Chai v5 で CommonJS サポートが廃止されました。require('chai') は使えません。
// ❌ v5 以降では動かない
const { expect } = require('chai');
// ✅ ESM の import を使う
import { expect } from 'chai';
tsconfig.json で "module": "ESNext" または "module": "NodeNext" を設定してください。
2. throw のアサーションには関数を渡す
よくあるミスとして、関数の実行結果を渡してしまうパターンがあります。
// ❌ 間違い:この時点で例外がスローされてテストがクラッシュする
expect(divide(1, 0)).to.throw();
// ✅ 正しい:関数でラップして渡す
expect(() => divide(1, 0)).to.throw();
3. equal と deep.equal の使い分け
// ❌ 参照が異なるため失敗する
expect({ a: 1 }).to.equal({ a: 1 });
// ✅ 中身を比較するなら deep.equal
expect({ a: 1 }).to.deep.equal({ a: 1 });
// ✅ eql は deep.equal のショートハンド
expect({ a: 1 }).to.eql({ a: 1 });
4. プラグインで機能を拡張する
chai-as-promised(Promise のアサーション)や chai-http(HTTP テスト)など、公式・コミュニティプラグインが豊富です。
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
use(chaiAsPromised);
// Promise の解決値を検証
await expect(Promise.resolve(42)).to.eventually.equal(42);
await expect(Promise.reject(new Error('fail'))).to.be.rejectedWith('fail');
5. チェーン内の「言語チェーン」は意味を持たない
to、be、been、is、that、which、and、has、have、with、at、of、same、but、does は可読性のためだけに存在し、アサーションの動作には影響しません。
// 以下はすべて同じ意味
expect(42).to.be.a('number');
expect(42).a('number');
expect(42).to.be.to.be.to.be.a('number'); // 冗長だが動く
まとめ
Chai は、expect・should・assert の3スタイルを備えた柔軟なアサーションライブラリで、テストコードの可読性を大幅に向上させます。テストフレームワークに依存しない設計と豊富なプラグインエコシステムにより、あらゆるプロジェクトのテスト基盤として信頼できる選択肢です。Vitest を使っている場合は内部的に Chai が組み込まれているため、すでにその恩恵を受けていることも覚えておくとよいでしょう。