Jest・Mocha・Chai:JavaScriptテストツールの棲み分け比較
はじめに
JavaScript のテストツールを調べると、まず名前が挙がるのが Jest・Mocha・Chai の 3 つです。しかしこの 3 つは「どれを選ぶか」という同列の選択肢ではありません。担当するレイヤーが違います。
- Jest … テストランナー + アサーション + モック + カバレッジを 1 つに束ねたオールインワン
- Mocha … テストランナー(テストの構造とライフサイクルだけを提供する)
- Chai … アサーションライブラリ(期待値の検証だけを担当する)
実際の選択は「Jest 単体」か「Mocha + Chai(+ Sinon などを足す)」かという構成単位の比較になります。本記事では各ツールの責務を切り分けたうえで、どんなプロジェクトならどちらが向くかを整理します。
テストツールが担う責務の分解
JavaScript のテストは、おおむね次の責務の積み重ねで成り立っています。
| 責務 | やること | Jest | Mocha | Chai |
|---|---|---|---|---|
| テストランナー | describe / it で構造化・実行 |
✅ | ✅ | ― |
| アサーション | 期待値を検証 | ✅ | ―(別途) | ✅ |
| モック / スタブ | 関数・モジュールの差し替え | ✅ | ―(Sinon 等) | ― |
| カバレッジ | 行・分岐の計測 | ✅ | ―(c8/nyc 等) | ― |
| スナップショット | 出力の記録・差分比較 | ✅ | ―(別途) | ― |
Jest は横一列をほぼ全部カバーします。Mocha はランナーに特化し、アサーションやモックは外部ライブラリと組み合わせる前提です。Chai はアサーションの一機能だけを提供するので、必ず何らかのランナーとセットで使います。
Mocha + Chai の構成
Mocha はテストの「枠」だけを提供します。期待値を検証する機能を持たないため、Chai を足します。
const { expect } = require("chai");
const { sum } = require("../src/sum");
describe("sum", () => {
it("adds two numbers", () => {
expect(sum(1, 2)).to.equal(3);
});
it("returns a number", () => {
expect(sum(1, 2)).to.be.a("number");
});
});
Chai は 3 スタイルのアサーションを提供するのが特徴です。
const chai = require("chai");
// 1. expect スタイル
chai.expect(value).to.equal(3);
// 2. should スタイル(Object.prototype を拡張する)
chai.should();
value.should.equal(3);
// 3. assert スタイル(Node.js 標準 assert に近い)
chai.assert.equal(value, 3);
expect(value).to.be.an("array").that.includes(2) のように自然言語に近く連結できる BDD スタイルが人気の理由です。
モックが必要になると Sinon を足します。
const sinon = require("sinon");
const { expect } = require("chai");
it("calls the callback once", () => {
const callback = sinon.spy();
doSomething(callback);
expect(callback.calledOnce).to.be.true;
});
カバレッジが欲しければ c8 か nyc を被せます。つまり Mocha 構成は「Mocha + Chai + Sinon + c8」のように積み上げていくのが基本形です。
Jest の構成
Jest は上記を 1 パッケージで賄います。
const { sum } = require("../src/sum");
describe("sum", () => {
it("adds two numbers", () => {
expect(sum(1, 2)).toBe(3);
});
});
モックも組み込みです。
it("calls the callback once", () => {
const callback = jest.fn();
doSomething(callback);
expect(callback).toHaveBeenCalledTimes(1);
});
// モジュール全体のモックも 1 行
jest.mock("../src/api");
カバレッジは --coverage を付けるだけ、スナップショットも標準装備です。
it("renders correctly", () => {
expect(renderComponent()).toMatchSnapshot();
});
jest --coverage
性能と実行モデルの違い
- Jest … テストファイルごとにワーカープロセスを分けて並列実行し、各ファイルを独立したモジュールレジストリで走らせる。ファイル間の状態汚染が起きにくい反面、プロセス分離のオーバーヘッドがある。
jsdomなどの環境を噛ませることが多く、ブラウザ API のモックが絡むと素の Node.js とは挙動が変わる。 - Mocha … デフォルトは単一プロセスで直列実行(
--parallelで並列化も可能)。軽量で起動が速く、Node.js の素の挙動に近いまま動く。
「なぜか CLI で動くのに Jest 環境だと落ちる」という差異は、この実行環境の違いに起因することが多いです。
どちらを選ぶか
Jest が向くケース
- React / React Native / Vue などフロントエンド中心(Create React App や Next.js 系はデフォルトで Jest 寄り)
- モック・カバレッジ・スナップショットをすぐ使いたい
- 設定を最小にして「入れたらすぐ書ける」状態を優先したい
- チームの経験値がばらつくので構成を 1 本に固定したい
Mocha + Chai が向くケース
- Node.js のバックエンド / ライブラリで素の実行環境に近い挙動でテストしたい
- Chai の BDD スタイルのアサーション表現にこだわりがある
- モックは Sinon、カバレッジは c8 など部品を自前で選びたい
- 起動の軽さ・実行速度を重視する小規模なテスト群
補足:Vitest という第 3 の選択肢
近年は Vite ベースのプロジェクトで Vitest という選択肢も広がっています。Jest 互換の API を持ちつつ Vite の変換パイプラインに乗るため、ESM / Vite 環境と相性良く使えます。本記事の軸からは外れるため、ここでは「Jest か Mocha+Chai か」に絞ります。
実装例:同じ関数を両構成で書く
// src/inventory.js
function applyDiscount(price, rate) {
if (rate < 0 || rate > 1) {
throw new Error("rate must be between 0 and 1");
}
return Math.round(price * (1 - rate));
}
module.exports = { applyDiscount };
Jest 版:
const { applyDiscount } = require("../src/inventory");
describe("applyDiscount", () => {
it("applies a 20% discount", () => {
expect(applyDiscount(1000, 0.2)).toBe(800);
});
it("throws on invalid rate", () => {
expect(() => applyDiscount(1000, 1.5)).toThrow("rate must be between 0 and 1");
});
});
Mocha + Chai 版:
const { expect } = require("chai");
const { applyDiscount } = require("../src/inventory");
describe("applyDiscount", () => {
it("applies a 20% discount", () => {
expect(applyDiscount(1000, 0.2)).to.equal(800);
});
it("throws on invalid rate", () => {
expect(() => applyDiscount(1000, 1.5)).to.throw("rate must be between 0 and 1");
});
});
テストの骨格(describe / it)はほぼ同じで、違いはアサーションの書き方(.toBe vs .to.equal、.toThrow vs .to.throw)と、Chai を import するかどうかに集約されます。
確認方法
# Jest npx jest npx jest --coverage # Mocha + Chai npx mocha "mocha/**/*.spec.js" npx c8 mocha "mocha/**/*.spec.js"
注意点
- Chai のバージョンと import 形式:新しいメジャーバージョンは ESM 前提になっているため、CommonJS の
requireでそのまま読めないことがあります。プロジェクトのモジュール形式(CJS / ESM)と Chai のバージョンを合わせて確認してください(仮置き: 使用中バージョンの README を参照)。 - Jest と ESM:Jest は歴史的に CommonJS 前提の設計で、純粋な ESM を扱うには追加設定が必要になることがあります。
- 「全部入り」の裏返し:Jest はグローバルに
describe/it/expectを注入し、jsdom環境を噛ませることが多いため、素の Node.js とは挙動が変わる場面があります。環境依存のバグを切り分けるときはこの前提を思い出すと早いです。 - 混ぜない:1 プロジェクト内で Jest と Mocha を併用すると設定・グローバルが衝突します。どちらかに寄せるのが原則です。
まとめ
- Jest・Mocha・Chai は同列の選択肢ではなく、担当レイヤーが違う。
- 実際の比較は「Jest 単体」vs「Mocha + Chai(+ Sinon + c8)」。
- Jest は全部入りで、フロントエンド・すぐ書き始めたいチーム向け。
- Mocha + Chai は部品を選べる構成で、Node.js バックエンド・素の実行環境・アサーション表現にこだわる場面向け。
- 迷ったら「構成を固定したいなら Jest、組み合わせの自由が欲しいなら Mocha + Chai」で切り分けるのが実務的。
