- 技術 💻
Jestの使い方|テストの書き方・モック・Vitestとの違いを実行結果で
結論
Jest は、npm install --save-dev jest で入れ、test() と expect() でテストを書き、npx jest で実行する JavaScript・TypeScript のテストフレームワークです。テストの実行・マッチャー・モック・カバレッジが1つに入っていて、追加の設定なしに始められます。
この記事でわかること
- インストールから describe・test・expect・マッチャー・モック・非同期・カバレッジまでの書き方と、Jest 30.5.2 で実行した出力の原文
- TypeScript で使う3通り(ts-jest・Babel・Node の型消去)と、TypeScript 7 で ts-jest が止まる理由
- Vitest 5 との違いの表、同じ1,000テストの実測、移行で実際に出たエラーと直し方
Jest は、テストファイルを探して、必要なら変換(Babel や ts-jest)し、複数のワーカーで並列に実行して、expect の結果をまとめて表示します。カバレッジも同じ流れの中で集めます。
flowchart TD
A[npx jest] --> B[*.test.js を探す<br>testMatch]
B --> C[変換<br>babel-jest / ts-jest]
C --> D[ワーカーで並列実行<br>describe / test]
D --> E{expect の判定}
E -->|すべて通る| P[PASS]
E -->|1つでも違う| F[FAIL<br>Expected / Received]
D --> G[カバレッジ<br>--coverage]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| jest とは | Meta 発の JavaScript テストフレームワーク。実行・マッチャー・モック・カバレッジが同梱 | Jestとは |
| jest 使い方(最初の一歩) | npm i -D jest → sum.test.js を書く → npx jest |
インストールと設定 |
| テストの書き方 | describe でまとめ、test に1つの確認、expect(値).マッチャー() で判定 |
テストの書き方 |
| toBe と toEqual の違い | toBe は同一性(Object.is)、toEqual は中身の比較 | マッチャー早見表 |
| jest.fn・jest.mock・jest.spyOn の違い | 関数を作る・モジュールごと差し替える・既存の関数を監視する | モック |
| 非同期のテスト | async/await か await expect(p).resolves。await を忘れない |
非同期処理のテスト |
| TypeScript で動かす | ts-jest(型チェックあり)か Babel(型チェックなし・速い)。TypeScript は 6 系で | TypeScriptで使う |
| jest vitest 違い | API はほぼ同じ。Vitest は ESM・Vite 前提で vi.*、Jest は CommonJS 前提で jest.* |
JestとVitestの違い |
確認環境:Ubuntu 24.04.4 LTS、Node.js 22.23.4、npm 10.9.9 で、空のプロジェクトに Jest 30.5.2 を npm install --save-dev jest で入れ、この記事のテストをすべて実行しました。TypeScript は ts-jest 29.4.15(TypeScript 6.0.3・7.0.2 の両方で試行)、@babel/core 8.0.9+@babel/preset-typescript 8.0.7、Jest 30.5.2 の型消去(transform: {})の3通りで動かし、ブラウザの API は jest-environment-jsdom 30.5.2(jsdom 26.1.0)で確かめました。比較対象の Vitest は 5.0.5(Vite 8.3.5)を同じプロジェクトに入れ、同じテストファイルを実行しています。実行時間は 2 CPU の環境で time を使って3回ずつ測りました。Jest 30 の変更点・ESM の扱い・TypeScript の設定は Jest 公式ドキュメントと公式ブログ、Vitest との API の差は Vitest 公式の移行ガイドで確かめています。図は mermaid 11 で描画を確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Jestの使い方|テストの書き方・モック・Vitestとの違いを実行結果で」
Jestとは(何ができるか・Jest 30の位置づけ)
この節では、Jest が何をするツールで、ほかのテストツールとどう違うのかが分かります。
Jest は、Meta(旧 Facebook)が作り、今は OpenJS Foundation の下で開発されている JavaScript のテストフレームワークです。 Mocha+Chai+Sinon のように複数のライブラリを組み合わせなくても、次の4つが1つのパッケージに入っています。
1テストランナー
テストファイルを探し、ファイルごとに別のワーカーで並列に実行する。--watch で変更したファイルだけ再実行できる。
2アサーション(expect)
toBe・toEqual・toThrow など、読んで意味が分かるマッチャーが揃っている。失敗したときの差分表示も同梱。
3モック
jest.fn・jest.mock・jest.spyOn・フェイクタイマーで、外部 API や時刻をテストから切り離せる。
4カバレッジ・スナップショット
--coverage で行・分岐・関数の網羅率を表にし、toMatchSnapshot で出力の変化を検知する。
今の版は Jest 30 系です。公式ブログによると、Jest 30(2025年6月)は Node 14・16・19・21 の対応を終え、TypeScript 5.4 以上が必要になり、--testPathPattern が --testPathPatterns に改名され、toBeCalled などマッチャーの別名が削除されました。手元で入れた 30.5.2 の package.json では、対応する Node は ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 でした。
| ツール | 役割 | Jest との関係 |
|---|---|---|
| Jest | 単体テスト・結合テスト(Node の中で実行) | ― |
| Vitest | 同じ役割。Vite・ESM 前提 | API はほぼ互換。後述の表で比較 |
| Mocha+Chai | ランナー+アサーションを別々に選ぶ | Jest はこれらを1つにしたもの |
| React Testing Library | React の画面をテストする道具 | Jest(または Vitest)の上で動く |
| Playwright・Cypress | 本物のブラウザで画面を操作する E2E テスト | 役割が違う。両方使う |
ブラウザで画面を操作する E2E テストは、関連記事「Playwright入門|E2Eテストの書き方とCI実行まで実行結果つきで解説」で扱っています。
Jestのインストールと設定(npm・package.json・jest.config.js)
この節では、空のプロジェクトに Jest を入れて、最初のテストが動くまでの手順が分かります。
開発用の依存として入れる
npm install --save-dev jest(npm i -D jest でも同じ)。手元では 30.5.2 が入り、npx jest --version が 30.5.2 を返しました。yarn・pnpm・bun でも同じ名前です。
package.json の scripts に登録する
"test": "jest" を書くと npm test で実行できます。npm init -y が作る "test": "echo \"Error: no test specified\" && exit 1" を書き換えます。
テストファイルを置く
既定で *.test.js・*.spec.js・__tests__/ の中が対象です(.ts・.mjs・.cts も)。設定ファイルは無くても動きます。
必要になったら jest.config.js を作る
公式の案内は npm init jest@latest で質問に答えて生成する方法です。手で書くなら次の数行から始めます。
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'node', // ブラウザの API が要るなら 'jsdom'
collectCoverageFrom: ['src/**/*.js', '!src/**/*.test.js'],
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
},
};
npx jest --showConfig で確かめた既定値のうち、覚えておくとよいものです。
| 設定 | 既定値(30.5.2) | 意味 |
|---|---|---|
| testMatch | **/__tests__/**/*.?([mc])[jt]s?(x)、**/?(*.)+(spec|test).?([mc])[jt]s?(x) |
テストとみなすファイル |
| testEnvironment | node | document を使うなら jsdom を別途入れる |
| transform | babel-jest(Babel の設定があれば変換) | TypeScript・ESM をどう読むか |
| coverageProvider | babel | v8 にすると変換なしで計測 |
| clearMocks | false | true にするとテストごとに呼び出し履歴を消す |
| cacheDirectory | /tmp/jest_0(Linux) | 変換結果のキャッシュ。--no-cache で無視 |
Jestのテストの書き方(describe・test・expect)と実行結果
この節では、最小のテストの書き方と、通ったとき・失敗したときに画面に何が出るのかが分かります。
テストは「test('説明', () => { expect(実際の値).マッチャー(期待する値); })」の形で書きます。 describe は関係するテストをまとめる入れ物で、it は test の別名です。
// src/sum.js
function sum(a, b) {
return a + b;
}
module.exports = { sum };
// src/sum.test.js
const { sum } = require('./sum');
describe('sum', () => {
test('1 + 2 は 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('小数の足し算', () => {
expect(sum(0.1, 0.2)).toBeCloseTo(0.3);
});
});
$ npx jest src/sum.test.js
PASS src/sum.test.js
sum
✓ 1 + 2 は 3 (3 ms)
✓ 小数の足し算 (1 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.42 s, estimated 1 s
Ran all test suites matching src/sum.test.js.
期待値をわざと 4 にして失敗させた出力です。どの行の、どの expect が、何を受け取ったか(Received)まで出るので、失敗の原因はこの画面だけで分かります。
FAIL src/fail.test.js
✕ わざと失敗させる (5 ms)
● わざと失敗させる
expect(received).toBe(expected) // Object.is equality
Expected: 4
Received: 3
2 |
3 | test('わざと失敗させる', () => {
> 4 | expect(sum(1, 2)).toBe(4);
| ^
5 | });
at Object.toBe (src/fail.test.js:4:21)
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 2 passed, 3 total
よく使うコマンドラインの指定
| 指定 | 動き(手元で確認) |
|---|---|
npx jest src/sum.test.js |
パスに一致するファイルだけ実行 |
npx jest -t '税込み' |
テスト名に一致するものだけ実行。ほかは skipped と数えられる |
npx jest --testPathPatterns sum |
パスの正規表現。以前の --testPathPattern は「replaced by --testPathPatterns」のエラー |
npx jest --watch |
変更したファイルに関係するテストを再実行。git の無い場所では「--watch is not supported without git/hg, please use --watchAll」 |
npx jest --verbose |
テスト名を1行ずつ表示(ファイルが1つのときは既定で表示) |
npx jest --coverage |
カバレッジの表を表示し、coverage/ に HTML を出す |
npx jest --runInBand(-i) |
並列にせず1プロセスで実行。デバッグやメモリ不足のとき |
npx jest --detectOpenHandles |
終了を妨げている接続・タイマーの場所を表示 |
test.each・skip・todo・only
同じ関数に違う入力を渡すテストは test.each でまとめます。%p は値を整形して埋め込む書き方で、Jest 30 では %$ で連番も入れられます。
test.each([
[20, null],
[-1, '0以上にしてください'],
['20', '数値を入れてください'],
])('validateAge(%p) は %p', (input, expected) => {
expect(validateAge(input)).toBe(expected);
});
test.todo('151 のとき'); // これから書く印
test.skip('あとで直す', () => {}); // 一時的に飛ばす
PASS src/validate.test.js
validateAge
✓ validateAge(20) は null (4 ms)
✓ validateAge(-1) は "0以上にしてください" (1 ms)
✓ validateAge("20") は "数値を入れてください" (1 ms)
○ skipped あとで直す
✎ todo 151 のとき
Tests: 1 skipped, 1 todo, 3 passed, 5 total
test.only を付けると、そのファイルの中でそれだけが実行されます。コミットに残すと他のテストが動かなくなるため、作業中だけ使います。
Jestのマッチャー早見表(toBe・toEqual・toThrow…)
この節では、よく使うマッチャーの意味と、迷いやすい toBe と toEqual の違いが分かります。
次の表のマッチャーは、すべて手元で1つのテストファイルにまとめて実行し、通ることを確かめています。
| 分類 | マッチャー | 意味・例 |
|---|---|---|
| 等しい | toBe(v) |
Object.is で同じ。数値・文字列・同じ参照のオブジェクト |
| 等しい | toEqual(v) |
中身を再帰的に比較。undefined のプロパティは無視 |
| 等しい | toStrictEqual(v) |
toEqual より厳密。{a:1, b:undefined} と {a:1} は違う |
| 真偽・null | toBeTruthy()・toBeFalsy()・toBeNull()・toBeUndefined()・toBeDefined() |
'0' は truthy、0 は falsy |
| 数値 | toBeGreaterThan(n)・toBeGreaterThanOrEqual(n)・toBeLessThan(n)・toBeCloseTo(n, 桁) |
0.1 + 0.2 は toBe(0.3) では失敗し、toBeCloseTo(0.3) なら通る |
| 文字列 | toMatch(/re/)・toContain('部分') |
'Hello Jest' は toMatch(/jest/i) |
| 配列・集合 | toContain(v)・toHaveLength(n)・toContainEqual(obj) |
[1,2,3] は toContain(2)、toHaveLength(3) |
| オブジェクト | toMatchObject(部分)・toHaveProperty('key', v)・toBeInstanceOf(Class) |
一部のプロパティだけ確かめる |
| 例外 | toThrow()・toThrow('文言')・toThrow(/re/)・toThrow(ErrorClass) |
関数そのものを渡す。expect(() => f()) |
| 非同期 | .resolves・.rejects |
await expect(p).resolves.toBe(v) |
| モック | toHaveBeenCalled()・toHaveBeenCalledTimes(n)・toHaveBeenCalledWith(...)・toHaveBeenLastCalledWith(...) |
jest.fn の呼び出しを確かめる |
| 非対称 | expect.any(Number)・expect.anything()・expect.arrayContaining([...])・expect.stringMatching(/re/)・expect.arrayOf(...)(30 で追加) |
toEqual の中で「型だけ合えばよい」値に使う |
| 否定 | .not |
expect(x).not.toBe(y) |
オブジェクトの比較
expect({ a: 1 }).toBe({ a: 1 })。中身が同じでも別の参照なので失敗するtoEqual か toStrictEqual。Jest も失敗時に「replace "toBe" with "toStrictEqual"」と案内する手元の失敗出力:Expected: {"a": 1}/Received: serializes to the same string
toEqual が失敗したときは、差分が -(期待)と +(実際)で出ます。
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 2
Object {
"id": 1,
- "name": "bob",
+ "name": "alice",
"tags": Array [
"a",
+ "b",
],
}
toThrow に expect(f()) と書いて先に呼んでしまうと、例外が expect に届く前に投げられ、テストは「boom」というエラーそのもので失敗しました。expect(f) か expect(() => f()) と、関数を渡します。
beforeEach・afterEach・beforeAll・afterAll(フックの実行順)
この節では、テストの前後に動く4つのフックが、どの順に呼ばれるかが分かります。
beforeAll・afterAll はファイル(または describe)で1回、beforeEach・afterEach はテストごとに毎回動きます。 console.log を入れて順番を確かめました。
beforeAll(() => console.log('beforeAll: DB 接続など1回だけ'));
afterAll(() => console.log('afterAll: 後片付け'));
beforeEach(() => console.log(' beforeEach: 毎回の準備'));
afterEach(() => console.log(' afterEach: 毎回の後始末'));
test('1つ目', () => console.log(' test 1'));
describe('内側', () => {
beforeEach(() => console.log(' beforeEach(内側)'));
test('2つ目', () => console.log(' test 2'));
});
beforeAll: DB 接続など1回だけ
beforeEach: 毎回の準備
test 1
afterEach: 毎回の後始末
beforeEach: 毎回の準備
beforeEach(内側)
test 2
afterEach: 毎回の後始末
afterAll: 後片付け
外側の beforeEach が内側より先に動きます。Jest の出力では、console.log の1つ1つに「どのファイルの何行目か」が添えられるので、ログの出どころで迷いません。
Jestのモック(jest.fn・jest.mock・jest.spyOn)
この節では、3つのモックの使い分けと、外部 API を呼ぶ関数をテストから切り離す書き方が分かります。
jest.fn()
何もしない関数を作り、呼ばれた回数・引数・戻り値を記録する。mockReturnValue・mockResolvedValue で戻り値を決める。
コールバックの確認に
jest.mock('パス')
モジュールの全エクスポートを自動で jest.fn に置き換える。呼び出しはファイルの先頭に巻き上げられる。
API・DB の呼び出しに
jest.spyOn(obj, 'name')
既存の関数を元の実装のまま監視する。mockReturnValue で差し替え、mockRestore で戻す。
Math.random・console に
jest.requireActual
jest.mock の factory の中で本物を読み込み、一部だけ差し替える(部分モック)。
1関数だけ置き換えに
// src/api.js(本物は HTTP で取りに行く)
async function fetchUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
module.exports = { fetchUser };
// src/user.js(テストしたい関数)
const api = require('./api');
async function displayName(id) {
const user = await api.fetchUser(id);
return user.nickname || user.name;
}
module.exports = { displayName };
// src/mock.test.js
const { displayName } = require('./user');
const api = require('./api');
jest.mock('./api'); // ./api の全エクスポートを自動でモックにする
describe('jest.fn', () => {
test('呼び出しを記録し、戻り値を決められる', () => {
const fn = jest.fn().mockReturnValue(10);
fn('a', 1);
fn('b', 2);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith('a', 1);
expect(fn.mock.calls).toEqual([['a', 1], ['b', 2]]);
expect(fn.mock.results[0].value).toBe(10);
});
test('mockReturnValueOnce は1回だけ', () => {
const fn = jest.fn(x => x * 2).mockReturnValueOnce(0);
expect(fn(5)).toBe(0); // 1回目だけ 0
expect(fn(5)).toBe(10); // 2回目から実装
});
});
describe('jest.mock(モジュールの差し替え)', () => {
beforeEach(() => jest.clearAllMocks());
test('nickname があればそれを返す', async () => {
api.fetchUser.mockResolvedValue({ name: 'Alice', nickname: 'ali' });
await expect(displayName(1)).resolves.toBe('ali');
expect(api.fetchUser).toHaveBeenCalledWith(1);
});
test('API の失敗はそのまま投げる', async () => {
api.fetchUser.mockRejectedValue(new Error('HTTP 500'));
await expect(displayName(1)).rejects.toThrow('HTTP 500');
});
});
describe('jest.spyOn(元の実装を残して監視)', () => {
test('Math.random を固定する', () => {
const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
expect(Math.random()).toBe(0.5);
spy.mockRestore(); // 元に戻す
expect(Math.random()).not.toBe(0.5);
});
});
PASS src/mock.test.js
jest.fn
✓ 呼び出しを記録し、戻り値を決められる (6 ms)
✓ mockReturnValueOnce は1回だけ (1 ms)
jest.mock(モジュールの差し替え)
✓ nickname があればそれを返す (2 ms)
✓ API の失敗はそのまま投げる (3 ms)
jest.spyOn(元の実装を残して監視)
✓ Math.random を固定する (1 ms)
ネットワークには一度もつながず、fetch の中身を書き換えずに、displayName の分岐を両方テストできました。一部だけ差し替える部分モックはこう書きます。
jest.mock('./validate', () => ({
...jest.requireActual('./validate'), // 本物を残しつつ
isAdult: jest.fn(() => true), // isAdult だけ差し替える
}));
clearAllMocks・resetAllMocks・restoreAllMocks の違い
| 呼び出し | 消えるもの | 残るもの |
|---|---|---|
mockClear/clearAllMocks |
呼び出し履歴(calls・results) | 実装・戻り値の設定 |
mockReset/resetAllMocks |
履歴+実装(undefined を返す空の関数になる) | モックであること |
mockRestore/restoreAllMocks |
履歴+実装+モック自体 | spyOn の対象は元の関数に戻る |
手元で jest.fn(() => 'impl') を mockReset() したあとの戻り値は undefined でした。Vitest の mockReset は元の実装('impl')に戻るため、移行で動きが変わる点です。スタブ・モック・スパイの言葉の違いは、関連記事「テストダブルとは|スタブ・モック・スパイ・フェイクの違いと使い分け」で扱っています。
フェイクタイマーと時刻の固定(jest.useFakeTimers)
この節では、setTimeout を待たずにテストを終わらせる方法と、現在時刻を固定する方法が分かります。
jest.useFakeTimers() を呼ぶと、setTimeout・setInterval・Date がすべて偽物になり、jest.advanceTimersByTime(ms) で時間を進められます。 1秒待つデバウンスのテストが、実際には数ミリ秒で終わります。
function debounce(fn, ms) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}
describe('フェイクタイマー', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
test('setTimeout を待たずに進める', () => {
const fn = jest.fn();
const d = debounce(fn, 1000);
d('a'); d('b'); d('c');
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(999);
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith('c');
});
test('現在時刻を固定する', () => {
jest.setSystemTime(new Date('2026-10-29T09:00:00+09:00'));
expect(new Date().toISOString()).toBe('2026-10-29T00:00:00.000Z');
expect(Date.now()).toBe(1793232000000);
});
test('Promise と組み合わせるときは advanceTimersByTimeAsync', async () => {
const wait = ms => new Promise(r => setTimeout(r, ms));
let done = false;
wait(500).then(() => { done = true; });
await jest.advanceTimersByTimeAsync(500);
expect(done).toBe(true);
});
});
PASS src/timers.test.js
フェイクタイマー
✓ setTimeout を待たずに進める (5 ms)
✓ 現在時刻を固定する (2 ms)
✓ Promise と組み合わせるときは advanceTimersByTimeAsync (4 ms)
afterEachでuseRealTimers()に戻さないと、同じファイルの別のテストで本物の setTimeout が動かなくなります。- Promise の
thenを待つ処理があるときは、同期のadvanceTimersByTimeではなくawait jest.advanceTimersByTimeAsync()を使います。 - Jest 30 では
jest.advanceTimersToNextFrame()(requestAnimationFrame 用)と、Temporal API の偽装(30.4)が加わりました。
非同期処理のテスト(async/await・resolves・rejects)
この節では、Promise を返す関数のテストの書き方と、await を忘れたときに何が起きるかが分かります。
const fetchData = () => new Promise(r => setTimeout(() => r('peanut butter'), 10));
const fetchError = () => Promise.reject(new Error('network down'));
test('async/await', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('.resolves / .rejects', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
await expect(fetchError()).rejects.toThrow('network down');
});
test('Promise を return する', () => {
return fetchData().then(data => expect(data).toBe('peanut butter'));
});
test('コールバック形式は done を使う', done => {
setTimeout(() => {
try { expect(1).toBe(1); done(); } catch (e) { done(e); }
}, 10);
});
test('try/catch で失敗を確かめるときは expect.assertions', async () => {
expect.assertions(1);
try { await fetchError(); } catch (e) { expect(e.message).toMatch('down'); }
});
5つとも通りました。いちばん多いつまずきは、expect(p).rejects の前の await を忘れることです。 以前の版ではテストが先に終わって「通ったように見える」のが問題でしたが、手元の 30.5.2 では、await の無い .rejects.toThrow('別の文言') が失敗として検出されました。それでも、テストの終了より後に判定が走る形は読みにくいため、必ず await を付けます。
テストが既定の 5 秒(testTimeout)を超えたときの原文です。3番目の引数でテストごとに延ばせます。
● 5秒を超えるテスト
thrown: "Exceeded timeout of 1000 ms for a test.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
このとき、終わっていない setTimeout が残っていたので、最後に次の文言も出ました。サーバーや DB の接続を閉じ忘れたときも同じ文言です。
Jest did not exit one second after the test run has completed.
'This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
--detectOpenHandles を付けると、閉じ忘れた場所が行番号つきで出ました。
Jest has detected the following 1 open handle potentially keeping Jest from exiting:
● TCPSERVERWRAP
1 | const net = require('net');
2 | test('サーバーを閉じ忘れる', done => {
> 3 | const server = net.createServer().listen(0, '127.0.0.1', () => {
| ^
カバレッジの出力と coverageThreshold
この節では、--coverage の表の読み方と、CI でカバレッジが下がったら失敗させる設定が分かります。
$ npx jest --coverage
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 70 | 66.66 | 60 | 75 |
api.js | 20 | 0 | 0 | 25 | 3-5
sum.js | 100 | 100 | 100 | 100 |
user.js | 100 | 50 | 100 | 100 | 5
validate.js | 77.77 | 87.5 | 50 | 83.33 | 8
-------------|---------|----------|---------|---------|-------------------
|| の片側しか通っていない- api.js の 20%:
jest.mock('./api')で差し替えたので、本物の3〜5行目は一度も通っていません。モックした側のカバレッジは下がって当然で、そのモジュール自身のテストを別に書きます。 - user.js の Branch 50%:
user.nickname || user.nameで、nickname が無い場合のテストが無いためです。行の網羅率だけ見ていると気づけません。 - validate.js の Uncovered Line 8:
isAdultをどのテストも呼んでいません。
coverageThreshold を設定しておくと、基準を下回ったときに終了コードが 1 になります(手元では exit=1)。CI で「テストは通ったのにカバレッジが落ちた」を止められます。
Jest: Coverage for statements (70%) does not meet "global" threshold (80%)
Jest: Coverage for branches (66.66%) does not meet "global" threshold (80%)
Jest: Coverage for lines (75%) does not meet "global" threshold (80%)
Jest: Coverage for functions (60%) does not meet "global" threshold (80%)
CI に組み込む流れは、関連記事「CI/CDとは|GitHub Actionsで最初のパイプラインを作るまで」で扱っています。
スナップショットテスト
toMatchSnapshot() は、初回の実行で __snapshots__/*.snap に出力を保存し、2回目からはそれと比べます。出力を変えたら -u で更新します。CI(--ci)では新しいスナップショットを書かず、「New snapshot was not written. The update flag must be explicitly passed to write a new snapshot.」で失敗しました。
- Snapshot - 1
+ Received + 1
- "<article class="card"><h2>Alice</h2><p>admin</p></article>"
+ "<article class="card"><h2>Alice</h2><p>member</p></article>"
› 1 snapshot failed.
TypeScriptで使う(ts-jest・Babel・Node の型消去)
この節では、.ts のテストを動かす3通りの方法と、それぞれの速さ・型チェックの有無が分かります。
何も設定せずに .ts を実行すると、既定の babel-jest が TypeScript を読めずに止まります。
FAIL ts/price.test.ts
● Test suite failed to run
SyntaxError: /dev/shm/t19/proj/ts/price.test.ts: Unexpected token, expected "," (1:21)
> 1 | import { total, type Item } from './price';
| ^
| 方法 | 型チェック | 手元の実行時間(2ファイル) | 入れるもの |
|---|---|---|---|
| ts-jest(isolatedModules: false) | あり。型エラーで FAIL | 2.2〜2.5 秒 | typescript@6・ts-jest・@types/jest |
| ts-jest(isolatedModules: true) | なし(変換だけ) | 0.3 秒 | 同上 |
| Babel(@babel/preset-typescript) | なし。tsc --noEmit を別に回す |
0.7 秒 | @babel/core・@babel/preset-env・@babel/preset-typescript |
| Node の型消去(transform: {}、30.5.2〜) | なし | 0.2〜0.3 秒 | 追加なし(.cts か ESM の設定) |
ts-jest:型エラーをテストの失敗として出す
$ npm install --save-dev typescript@6 ts-jest @types/jest
$ npx ts-jest config:init
Jest configuration written to "/dev/shm/t19/proj/jest.config.js".
// 生成された jest.config.js
const { createDefaultPreset } = require("ts-jest");
const tsJestTransformCfg = createDefaultPreset().transform;
/** @type {import("jest").Config} **/
module.exports = {
testEnvironment: "node",
transform: { ...tsJestTransformCfg },
};
price: '100' と文字列を渡した呼び出しは、ts-jest ではテストが動く前に止まりました。
FAIL ts/typeerr.test.ts
● Test suite failed to run
ts/typeerr.test.ts:4:32 - error TS2322: Type 'string' is not assignable to type 'number'.
4 expect(total([{ name: 'pen', price: '100', qty: 1 }])).toBe(110);
~~~~~
tsconfig.json は、テスト用に次の形にしました。types: ["jest"] が無いと test・expect が「Cannot find name 'test'」になります。
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["jest"]
}
}
- 1TypeScript 7 では ts-jest が動かない7.0.2 を入れると「The TypeScript compiler "typescript" (version 7.0.2) does not expose the JavaScript compiler API required by ts-jest」。ts-jest 29.4.15 の peerDependencies は typescript >=4.3 <7。
npm i -D typescript@6(6.0.3)で動いた - 2tsc --init の既定値は Jest 向きではないTypeScript 6 の
tsc --initは module: nodenext・verbatimModuleSyntax: true・types: [] を書く。そのままだと TS1295(CommonJS で import は書けない)と TS2593(test が無い)になった - 3isolatedModules: true だと型チェックされないts-jest は tsconfig の isolatedModules を見て、true なら1ファイルずつ変換だけ行う。型エラーのテストが PASS になったのはこのため。速さを取るなら true+別で tsc
- 4@types/jest と Jest の版を揃える公式は「できるだけ近い版を」としている。手元は Jest 30.5.2 に @types/jest 30.0.0。import で書くなら
@jest/globals(本体に同梱)でもよい
Babel:変換だけ行い、型は tsc で見る
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};
jest.config.js の transform は書かず、babel-jest に任せます。型エラーのある呼び出しも PASS になるので(手元で確認)、型は tsc --noEmit を CI で別に回します。@babel/core 8 の対応 Node は ^22.18.0 || >=24.11.0 でした。
Node の型消去:Jest 30.5.2 の新しい道
Jest 30.5.2 の CHANGELOG には、どの transformer も .ts を扱わないとき、Node の型消去(type stripping)を使うと書かれています。transform: {} にして試しました。
| 書き方 | 結果 |
|---|---|
.cts で require/module.exports |
PASS。追加のパッケージなし、0.34 秒 |
.ts で import(package.json に type: module なし) |
「Must use import to load ES Module」で FAIL |
同上+extensionsToTreatAsEsm: ['.ts']+NODE_OPTIONS=--experimental-vm-modules |
PASS。0.24 秒 |
型消去は型を空白に置き換えるだけなので、enum や constructor(private x) のような「出力が変わる構文」は動きません。Node 側の型消去の範囲は、関連記事「TypeScriptをNode.jsで実行|tsx・ts-node・型ストリップ」で扱っています。
ESM・jsdom・Jest 30で注意すること
この節では、import で書いたテストが動かない理由と、ブラウザの API を使うテストの設定が分かります。
ESM(import/export)
"type": "module" のプロジェクトで import を使ったテストを、設定なしで実行した結果です。Jest 30.5 のエラーは、直し方が3つ並ぶ形になっています。
FAIL ./sum.test.js
● Test suite failed to run
Must use import to load ES Module: /dev/shm/t19/esm/sum.test.js
The file contains ESM syntax (import/export) that could not be executed as CommonJS. Either:
- Configure a transform (e.g. babel-jest) that compiles this file to CommonJS (see https://jestjs.io/docs/code-transformation)
- If the file is in "node_modules", allow it to be transformed by adjusting "transformIgnorePatterns" (see https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)
- Use Node v24.9+ where Jest supports require(esm) natively (see https://jestjs.io/docs/ecmascript-modules#require-of-esm)
A. CommonJS に変換する(いちばん無難)
babel.config.cjs に @babel/preset-env を書く
jest.mock の巻き上げがそのまま使える
手元では設定ファイルを置くだけで PASS
B. ESM のまま動かす(実験的)
NODE_OPTIONS=--experimental-vm-modules npx jest
jest.mock は効かず jest.unstable_mockModule を使う
手元では ExperimentalWarning つきで PASS
公式ドキュメントは ESM 対応を「experimental」と明記しています。jest オブジェクトは ESM では自動で入らないため、import { jest } from '@jest/globals' で読み込みます。
jsdom(document・window を使うテスト)
既定の環境は node なので、document を使うと次の文言で失敗します。
The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
Consider using the "jsdom" test environment.
ReferenceError: document is not defined
Jest 28 以降、jsdom は同梱されていません。npm i -D jest-environment-jsdom(30.5.2・jsdom 26.1.0)を入れ、testEnvironment: 'jsdom' か、ファイルの先頭に /** @jest-environment jsdom */ と書きます。入れずに指定すると「Test environment jest-environment-jsdom cannot be found. … As of Jest 28 "jest-environment-jsdom" is no longer shipped by default」になりました。React の画面のテストは、関連記事「React Testing Library使い方|getBy・findByの違い」で扱っています。
Jest 30 で変わったこと(手元で確かめた分)
| 変更 | 手元の結果 |
|---|---|
| マッチャーの別名の削除 | expect(fn).toBeCalled() が「TypeError: expect(...).toBeCalled is not a function」。toHaveBeenCalled に直す |
--testPathPattern の改名 |
「Option "testPathPattern" was replaced by "--testPathPatterns"」 |
expect.arrayOf・test.each の %$ |
両方 PASS |
| AI コーディングエージェントの中では出力が簡潔になる | AI_AGENT・CLAUDECODE などの環境変数があると、PASS の行とテスト名が省かれ、集計と失敗の詳細だけになった。--reporters=default で元に戻る |
最後の点は、@jest/core 30.5.2 のソースに AGENT_ENV_VARS(AI_AGENT・CLAUDE_CODE・CLAUDECODE・CODEX_SANDBOX・CURSOR_AGENT・GEMINI_CLI など)の一覧があり、見つかると agent レポーターが選ばれる作りでした。Claude などから Jest を動かして「PASS の行が出ない」と感じたら、この動きです。
JestとVitestの違い(表・実測・移行手順)
この節では、2つの違いがどこにあるのか、同じテストを両方で動かした結果と、移行で実際に出たエラーが分かります。
| 観点 | Jest 30.5.2 | Vitest 5.0.5 |
|---|---|---|
| 前提 | CommonJS。ESM は実験的 | ESM・Vite。vite.config を共有できる |
| 対応 Node(package.json) | ^18.14 ‖ ^20 ‖ ^22 ‖ >=24 | ^22.12 ‖ ^24 ‖ >=26 |
| グローバル API | 既定で describe・test・expect が使える | 既定は import。globals: true で同じに |
| モック | jest.fn・jest.mock・jest.spyOn |
vi.fn・vi.mock・vi.spyOn。vi.mock は import にだけ効く |
| TypeScript | ts-jest か Babel か型消去 | 追加なし(esbuild で変換、型チェックなし) |
| mockReset | undefined を返す空の関数に | 元の実装に戻る |
| done コールバック | 使える | 使えない(async に書き換え) |
| フックの実行順 | 書いた順 | 既定はスタック(後入れ先出し)。sequence.hooks: 'list' で揃う |
| スナップショット | __snapshots__/*.snap |
同じ形式。手元では Jest の .snap をそのまま読めた |
| jsdom | jest-environment-jsdom を別に入れる | jsdom を入れて environment: 'jsdom'。@jest-environment の docblock も読めた |
同じ1,000テストを実行した時間
50 ファイル × 20 テスト(計 1,000)を生成して、2 CPU の環境で3回ずつ測りました。
| 実行 | ツールの報告 | コマンド全体(wall) |
|---|---|---|
| Jest(既定) | Time 2.0〜2.4 s | 2.9〜3.4 s |
Jest --no-cache |
Time 2.5 s | 3.8 s |
| Vitest(既定・ファイルごとに隔離) | Duration 8.6〜9.7 s | 9.3〜10.5 s |
Vitest --no-isolate |
Duration 0.59 s | 1.4 s |
| 1ファイル(sum.test.js) | Jest 0.15〜0.19 s/Vitest 0.17 s | どちらも約 1.0 s |
「Vitest は速い」は条件つきです。 この環境では、既定の設定ではファイルごとの隔離(新しいワーカーの文脈を作る)に時間がかかり、Jest の方が速く終わりました。隔離を切ると Vitest が最速です。CPU 数・ファイル数・変換の量で逆転するので、自分のプロジェクトで測るのが確実です。
移行で実際に出たエラーと直し方
Jest で書いたテストファイルを、そのまま Vitest(globals: true)で実行しました。
✓ src/matchers.test.js (6 tests) 15ms
❯ src/mock.test.js (0 test)
❯ src/timers.test.js (3 tests | 3 failed) 9ms
✓ src/async.test.js (5 tests) 41ms
✓ src/snap.test.js (2 tests) 15ms
✓ src/sum.test.js (2 tests) 5ms
FAIL src/mock.test.js [ src/mock.test.js ]
ReferenceError: jest is not defined
❯ src/mock.test.js:4:1
4| jest.mock('./api');
- 1jest.* を vi.* に置き換え、import { vi } from 'vitest' を足すマッチャー・非同期・スナップショット・test.each はそのまま通った
- 2require を import に書き換えるvi.mock は import にだけ効く。require のままだと「mockResolvedValue is not a function」になった
- 3テスト対象のモジュールも ESM にするCommonJS 同士の require の内側は差し替わらず、本物の fetch が走った(getaddrinfo ENOTFOUND)
- 4mockReset・フック順・done の違いを見直す公式の移行ガイドに一覧がある。jest.setTimeout は vi.setConfig({ testTimeout })
- 5設定ファイルは .mjs か type: modulevitest.config.js を CommonJS の package.json で読むと「ESM syntax in a file loaded as CommonJS」の警告
1〜3 を済ませた mock.test.js と timers.test.js は、Vitest で 8 テストすべて通りました。
どちらを選ぶか(判断表)
| 状況 | 向いているもの | 理由 |
|---|---|---|
| Vite(React・Vue・Svelte のテンプレート)で作っている | Vitest | vite.config のエイリアス・環境変数をそのまま使える |
| CommonJS の Node アプリ・Express・既存の Jest 資産が多い | Jest | jest.mock の巻き上げ・require がそのまま動く |
| Next.js・Create React App 由来 | どちらも | Next.js は Jest・Vitest の両方に公式の手順がある |
| Node 18・20 を使い続ける | Jest | Vitest 5 は Node 22.12 以上 |
| ESM のライブラリを多く import する | Vitest | Jest は transformIgnorePatterns の調整が要る |
| TypeScript の型エラーもテストで止めたい | Jest+ts-jest | Vitest は変換だけ。型は tsc で別に |
Vitest 側のモックの書き方は、関連記事「Vitestモックの使い方|vi.mock・vi.fn・vi.spyOnの使い分け表」で扱っています。この記事は Jest の入口、あちらは Vitest のモックの詳細、という分担です。
Jestのよくあるエラー・つまずき(原文つき)
この節では、この記事を書く中で実際に出たエラーの原文と、それぞれの直し方が分かります。
| 原文(抜粋) | 原因 | 直し方 |
|---|---|---|
sh: 1: jest: not found |
npm test したが jest が入っていない | npm i -D jest。グローバルではなくプロジェクトに入れる |
Your test suite must contain at least one test. |
.test.js に test が1つも無い | test を書くか、ファイル名から .test を外す |
Cannot find module './summ' from 'src/missing.test.js' |
require のパスの誤り。拡張子・大文字小文字 | パスを直す。エイリアスなら moduleNameMapper |
SyntaxError: … Unexpected token, expected "," |
.ts を変換なしで読んだ | ts-jest か @babel/preset-typescript |
Must use import to load ES Module |
ESM を CommonJS として実行した | Babel で変換するか --experimental-vm-modules |
ReferenceError: document is not defined |
testEnvironment が node | jest-environment-jsdom を入れて jsdom に |
Test environment jest-environment-jsdom cannot be found |
Jest 28 以降は同梱されない | npm i -D jest-environment-jsdom |
Exceeded timeout of 5000 ms for a test. |
await 忘れ・本物の通信・長い処理 | モックにする。必要なら第3引数で延長 |
Jest did not exit one second after the test run has completed. |
サーバー・DB・タイマーの閉じ忘れ | --detectOpenHandles で場所を見て afterAll で閉じる |
expect(...).toBeCalled is not a function |
Jest 30 で別名が削除された | toHaveBeenCalled に置き換える |
--watch is not supported without git/hg |
git 管理の無いフォルダー | --watchAll を使う |
The TypeScript compiler "typescript" (version 7.0.2) does not expose the JavaScript compiler API |
TypeScript 7 と ts-jest 29 の組み合わせ | typescript@6 を入れる |
New snapshot was not written. |
--ci では新規スナップショットを書かない |
手元で一度実行して .snap をコミット |
どれも、Jest 30 の文言は「何をすればよいか」まで書いてあるので、まず原文を最後まで読みます。
よくある質問
Q. Jest とは何ですか?
A. JavaScript・TypeScript のテストフレームワークです。テストの実行、expect によるアサーション、モック、カバレッジ、スナップショットが1つのパッケージに入っています。npm install --save-dev jest で入れ、npx jest で実行します。
Q. test と it の違いは?
A. 同じ関数の別名です。it('returns 3', …) のように英語の文として読ませたいときに it を使う人が多いだけで、動きは変わりません。
Q. toBe と toEqual はどう使い分けますか?
A. 数値・文字列・真偽値は toBe、オブジェクトや配列の中身を比べるときは toEqual(厳密には toStrictEqual)です。expect({a:1}).toBe({a:1}) は別の参照なので失敗します。
Q. jest.mock と jest.spyOn はどちらを使いますか?
A. モジュールごと切り離すなら jest.mock、既存のオブジェクトの1つの関数を監視・一時的に差し替えるなら jest.spyOn です。spyOn は mockRestore() で元に戻せます。
Q. 非同期のテストで「通ったように見えるのに実は何も確かめていない」を防ぐには?
A. .resolves・.rejects の前に必ず await を書き、try/catch で確かめるときは expect.assertions(n) を先頭に置きます。手元の 30.5.2 では await の無い rejects も失敗として検出されましたが、書き方としては await を付けます。
Q. TypeScript で Jest を使うには何を入れますか?
A. 型チェックもテストで止めたいなら typescript@6・ts-jest・@types/jest を入れて npx ts-jest config:init。速さを優先するなら @babel/preset-typescript で変換だけ行い、型は tsc で別に見ます。TypeScript 7 は ts-jest 29 では動きません。
Q. カバレッジは何%を目指せばよいですか? A. 数字より「Branch が低いファイル」と「Uncovered Line」を見ます。手元でも、行は 100% なのに分岐が 50% のファイルがありました。基準を決めるなら coverageThreshold で CI に組み込み、少しずつ上げます。
Q. Jest と Vitest はどちらが速いですか? A. 条件によります。手元の 2 CPU・1,000 テストでは、既定の設定では Jest が 2 秒台、Vitest が 8〜9 秒台で、Vitest の隔離を切ると 0.6 秒でした。1 ファイルの起動はどちらも約 1 秒です。自分の環境で測るのが確実です。
Q. Jest から Vitest へ移行する手間は?
A. マッチャー・非同期・スナップショットはそのまま動きました。直すのは jest.* → vi.*、require → import、mockReset・フック順・done の違いです。CommonJS 中心のコードは、テスト対象も ESM にしないと vi.mock が効きません。
Q. import を使ったテストが「Must use import to load ES Module」になります。
A. Jest は CommonJS で実行するのが基本です。babel.config に @babel/preset-env を書いて変換するか、NODE_OPTIONS=--experimental-vm-modules で ESM のまま動かします。後者は公式も実験的としています。
Q. React のコンポーネントをテストするには? A. jest-environment-jsdom を入れて testEnvironment を jsdom にし、React Testing Library を組み合わせます。画面の操作まで含めた E2E は Playwright の役割です。
Q. CI で Jest を動かすときの注意は?
A. --ci を付けると新しいスナップショットを書かずに失敗させます。カバレッジは coverageThreshold で基準を決め、閉じ忘れの接続は --detectOpenHandles で見つけます。並列数は --maxWorkers で CI の CPU に合わせます。
まとめ
- Jest は
npm i -D jest→test()・expect()→npx jestで始められ、失敗時は Expected/Received と行番号まで表示される。 - toBe は同一性、toEqual は中身。toThrow には関数を渡す。非同期は
await expect(p).resolves/.rejects。 - jest.fn(関数)・jest.mock(モジュール)・jest.spyOn(監視)を使い分け、フェイクタイマーで setTimeout と時刻を固定する。mockReset は Jest では undefined、Vitest では元の実装に戻る。
- TypeScript は ts-jest(型チェックあり・2 秒台)、Babel(変換だけ・0.7 秒)、Node の型消去(30.5.2〜・0.3 秒)の3通り。TypeScript 7 は ts-jest 29 と組み合わせられない。
- Vitest とは API がほぼ同じで、違いは ESM 前提・vi.*・mockReset・フック順。速さは条件次第で、手元の 1,000 テストでは既定の Jest が速く、隔離を切った Vitest が最速だった。
参考・出典
確認日はいずれも 2026年10月29日です。
- Jest「Getting Started」:https://jestjs.io/docs/getting-started
- Jest「Using Matchers」:https://jestjs.io/docs/using-matchers
- Jest「Mock Functions」:https://jestjs.io/docs/mock-functions
- Jest「Timer Mocks」:https://jestjs.io/docs/timer-mocks
- Jest「Testing Asynchronous Code」:https://jestjs.io/docs/asynchronous
- Jest「Configuring Jest」:https://jestjs.io/docs/configuration
- Jest「ECMAScript Modules」:https://jestjs.io/docs/ecmascript-modules
- Jest「Jest 30: Faster, Leaner, Better」(公式ブログ):https://jestjs.io/blog/2025/06/04/jest-30
- Jest「CHANGELOG」(30.5.2 の型消去の項):https://github.com/jestjs/jest/blob/main/CHANGELOG.md
- ts-jest「Getting Started」:https://kulshekhar.github.io/ts-jest/docs/getting-started/installation
- Vitest「Migrating from Jest」:https://vitest.dev/guide/migration/jest
- Vitest「Migrating to Vitest 5.0」:https://vitest.dev/guide/migration.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Jestの使い方|テストの書き方・モック・Vitestとの違いを実行結果で」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。