chai の使い方

BDD/TDD assertion library for node.js and the browser. Test framework agnostic.

v6.2.2/週MITテスト
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Chai の使い方 — JavaScript/TypeScript テストのためのアサーションライブラリ

一言でいうと

Chai は、Node.js とブラウザの両方で動作する BDD/TDD 対応のアサーションライブラリです。expectshouldassert の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 を変更するため、nullundefined に対して使えません。特別な理由がなければ 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/);

類似パッケージとの比較

特徴ChaiNode.js assertJest expectVitest 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. equaldeep.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. チェーン内の「言語チェーン」は意味を持たない

tobebeenisthatwhichandhashavewithatofsamebutdoes は可読性のためだけに存在し、アサーションの動作には影響しません。

// 以下はすべて同じ意味
expect(42).to.be.a('number');
expect(42).a('number');
expect(42).to.be.to.be.to.be.a('number'); // 冗長だが動く

まとめ

Chai は、expectshouldassert の3スタイルを備えた柔軟なアサーションライブラリで、テストコードの可読性を大幅に向上させます。テストフレームワークに依存しない設計と豊富なプラグインエコシステムにより、あらゆるプロジェクトのテスト基盤として信頼できる選択肢です。Vitest を使っている場合は内部的に Chai が組み込まれているため、すでにその恩恵を受けていることも覚えておくとよいでしょう。