Jestの基本機能を超わかりやすく紹介
ソフトウェア開発において、「テスト」は船の航海における羅針盤や気象レーダーのようなものです。目的地(完成)に安全かつ確実に到達するためには、常に現在の位置を確認し、予期せぬ嵐(バグ)を避け、進路が正しいか(仕様を満たしているか)をチェックする必要があります。
しかし、「テストって難しそう」「面倒くさそう」と感じていませんか? 特にJavaScriptの世界は変化が早く、様々なライブラリやフレームワークが登場します。そんな中で、信頼性の高いコードを書くためには、効果的なテストツールが必要です。
そこで登場するのが Jest です!
Jestは、Facebook(現Meta)が開発したJavaScriptのテスティングフレームワークです。ReactやReact Nativeのテストに使われることが多いですが、Node.js、Vue.js、Angularなど、様々なJavaScriptプロジェクトで利用できます。
なぜJestがこれほど人気なのでしょうか? その理由は、「設定がほぼ不要で、すぐに使い始められる」「機能が豊富で、様々な種類のテストに対応できる」「エラーメッセージが分かりやすく、デバッグしやすい」といった点にあります。
この記事では、Jestの基本的な機能を「超わかりやすく」解説します。プログラミング初心者の方から、これからJestを始めてみようと思っている方まで、 Jestの強力な機能を理解し、自信を持ってテストコードを書けるようになることを目指します。
さあ、Jestという強力なツールを使って、あなたのコードをより堅牢で信頼性の高いものにする旅に出かけましょう!
なぜテストが必要なのか?
Jestの解説に入る前に、そもそもなぜコードにテストが必要なのかを考えてみましょう。
- バグの早期発見と修正: テストは、コードに潜むバグを開発の早い段階で見つけ出すための最も効果的な手段の一つです。後工程になればなるほど、バグの修正コストは増大します。
- コードの品質向上: テストを書く過程で、コードの設計を見直したり、改善点に気づいたりすることがよくあります。テストしやすいコードは、自然とモジュール化され、理解しやすく、保守しやすいコードになります。
- 自信を持って変更できる: コードを修正したり機能を追加したりする際に、「この変更で他の部分が壊れてしまわないか?」という不安はつきものです。テストスイートがあれば、変更後にテストを実行するだけで、既存機能への影響を確認できます。これはリファクタリングを進める上でも非常に重要です。
- 仕様の明確化: テストコードは、そのコードが「何をすべきか」を示す具体的な例となります。これは、チームメンバーや未来の自分自身に対する非常に明確なドキュメントの役割を果たします。
- チーム開発の効率化: テストがあることで、他の開発者が書いたコードの意図を理解しやすくなり、マージ時のコンフリクトやデグレードのリスクを減らすことができます。
このように、テストは単にバグを見つけるだけでなく、開発プロセス全体を改善し、最終的に高品質なソフトウェアをより早く、より確実に届けするために不可欠なのです。
そしてJestは、JavaScript/TypeScriptのプロジェクトでこれらの恩恵を最大限に享受するための優れた選択肢です。
Jestの導入:はじめの一歩
Jestを使う準備をしましょう。まずは必要なものをインストールし、簡単なテストを実行してみます。
準備:Node.jsとnpm/yarn
JestはNode.js環境で動作します。まずはNode.jsがインストールされていることを確認してください。ターミナル(コマンドプロンプト)で以下のコマンドを実行します。
bash
node -v
npm -v
またはyarnを使っている場合は
bash
yarn -v
もしインストールされていない場合は、Node.jsの公式サイトからダウンロードしてインストールしてください。npm (Node Package Manager) はNode.jsと一緒にインストールされます。yarnは別途インストールが必要です。
プロジェクトの作成
次に、Jestを導入するプロジェクトを作成します。今回は新しいプロジェクトとして始めますが、既存のプロジェクトに追加することももちろん可能です。
まず、作業用のディレクトリを作成し、そこに移動します。
bash
mkdir my-jest-project
cd my-jest-project
次に、プロジェクトを初期化します。これにより、プロジェクトの設定ファイルである package.json
が作成されます。
bash
npm init -y
または
bash
yarn init -y
-y
オプションをつけると、対話式の質問をスキップしてデフォルト設定で package.json
が作成されます。
Jestのインストール
プロジェクトの準備ができたら、Jestをインストールします。開発時のみに必要なツールなので、--save-dev
または -D
オプションをつけます。
bash
npm install --save-dev jest
または
bash
yarn add --dev jest
インストールが完了すると、package.json
の devDependencies
に jest
が追加されているはずです。
package.json
の設定
Jestをインストールしたら、テストを実行するための設定を package.json
に追加します。通常、scripts
セクションに test
コマンドを追加します。
package.json
を開き、以下のように編集します。
json
{
"name": "my-jest-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest" // ★ この行を追加または変更
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.0.0" // バージョンは異なる場合があります
}
}
これで、ターミナルで npm test
または yarn test
と入力するだけでJestが実行されるようになります。
簡単なテストコードの作成
いよいよJestでテストコードを書いてみましょう。テストファイルは、通常テスト対象のファイルと同じディレクトリに配置し、ファイル名の末尾に .test.js
または .spec.js
をつけます。Jestはデフォルトでこれらの命名規則のファイルを自動的に見つけて実行します。
例として、簡単な足し算を行う関数をテストしてみます。
まず、テスト対象のファイル src/math.js
を作成します。
“`javascript
// src/math.js
function add(a, b) {
return a + b;
}
// テストファイルから呼び出せるようにエクスポート
module.exports = add;
“`
次に、この関数をテストするためのファイル src/math.test.js
を作成します。
“`javascript
// src/math.test.js
// テスト対象の関数をインポート
const add = require(‘./math’);
// test 関数を使ってテストケースを定義
test(‘should add two numbers’, () => {
// expect 関数を使ってテスト対象のコードを実行し、
// その結果が期待通りかtoMatchersを使って比較する
expect(add(1, 2)).toBe(3);
});
“`
テストの実行
ファイルを作成したら、ターミナルでテストを実行してみましょう。
bash
npm test
または
bash
yarn test
実行結果は以下のようになるはずです。
“`
PASS src/math.test.js
✓ should add two numbers (2ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: … ms
Ran all test suites.
“`
PASS
と表示されれば、テストは成功です! これでJestを使うための最初のステップは完了です。
この簡単な例の中に、Jestの基本的な要素がいくつか含まれています。
require
/import
: テスト対象のコードを読み込むtest
またはit
: 個々のテストケースを定義する関数expect
: テスト対象のコードの「結果」や「状態」を指定する関数toBe
:expect
と組み合わせて使う「マッチャー」。期待する値と実際の結果を比較する
次の章では、これらのJestの基本的な機能をさらに詳しく掘り下げていきます。
Jestの基本的な機能詳解
Jestには、テストを効率的かつ効果的に書くための様々な機能が用意されています。ここでは、それらの主要な機能を一つずつ見ていきましょう。
describe
ブロック:テストのグループ化
複数の関連するテストケースがある場合、それらをまとめて整理することができます。そのために使うのが describe
関数です。
describe
は2つの引数を取ります。1つ目はそのグループの説明(文字列)、2つ目はテストケースを定義するコールバック関数です。
先ほどのmath.test.js
をdescribe
を使って書き直してみましょう。
“`javascript
// src/math.test.js
const add = require(‘./math’);
const subtract = require(‘./math’).subtract; // 別の関数があるとして
// ‘Addition’ という名前で足し算のテストをグループ化
describe(‘Addition’, () => {
test(‘should add two positive numbers’, () => {
expect(add(1, 2)).toBe(3);
});
test(‘should add a positive and a negative number’, () => {
expect(add(5, -3)).toBe(2);
});
test(‘should add two negative numbers’, () => {
expect(add(-1, -2)).toBe(-3);
});
// describeの中にさらにdescribeをネストすることも可能
describe(‘Addition with zero’, () => {
test(‘should add zero to a positive number’, () => {
expect(add(10, 0)).toBe(10);
});
test('should add zero to a negative number', () => {
expect(add(-10, 0)).toBe(-10);
});
});
});
// ‘Subtraction’ という名前で引き算のテストをグループ化(subtract関数があると仮定)
// describe(‘Subtraction’, () => {
// test(‘should subtract two numbers’, () => {
// expect(subtract(5, 2)).toBe(3);
// });
// // … 他の引き算テスト
// });
“`
describe
を使うことで、テストの構造が分かりやすくなります。テスト結果の出力も、グループごとに表示されるため、どの部分のテストが成功・失敗したのかが一目で分かります。
describe.only
と describe.skip
開発中に特定のグループのテストだけを実行したい場合や、一時的に特定のグループのテストをスキップしたい場合があります。そんなときは .only
や .skip
を使います。
describe.only('特定のグループ', () => { ... });
: これを指定したグループ「のみ」が実行され、他のグループはスキップされます。describe.skip('スキップしたいグループ', () => { ... });
: これを指定したグループは実行されず、スキップされます。
これらは一時的なデバッグや開発効率化のための機能です。コミットする際は元に戻すのを忘れないようにしましょう。
test
または it
ブロック:個別のテストケース
test
関数は、個々のテストケース(シナリオ)を定義するために使われます。describe
ブロックの内部、またはトップレベルで使われます。
test
も2つの引数を取ります。1つ目はそのテストケースの説明(文字列)、2つ目はテストの本体となるコールバック関数です。
``javascript
test
//または
it` を使用
test(‘その関数は正しい結果を返す’, () => {
// テストコード本体
});
it(‘その関数は正しい結果を返す’, () => {
// test と it は全く同じように動作します。
// どちらを使うかはチームのコーディング規約などに従うと良いでしょう。
});
“`
テストの説明文字列は、そのテストが「どのような状況で」「何を」「どうなるべきか」を明確に記述することが推奨されます。「should add two numbers」のように、仕様を記述するような形で書くと、テストコード自体がドキュメントとしても機能します。
test.only
と test.skip
describe
と同様に、個別のテストケースに対しても .only
や .skip
を使うことができます。
test.only('特定のテスト', () => { ... });
: これを指定したテスト「のみ」が実行され、他のテストはスキップされます。test.skip('スキップしたいテスト', () => { ... });
: これを指定したテストは実行されず、スキップされます。
こちらもデバッグなどで便利ですが、コミット時は注意が必要です。
Matchers (マッチャー):期待する結果との比較
Jestの最も重要な機能の一つがマッチャーです。expect(value)
でテスト対象の値を取り出した後、 .toBe(...)
や .toEqual(...)
のようなマッチャーを使って、その値が期待する条件を満たすかどうかを検証します。
マッチャーの種類は非常に豊富で、様々な比較が可能です。ここではよく使われる基本的なマッチャーを紹介します。
基本的なマッチャー
.toBe(value)
: 厳密な等価性(===
)をチェックします。プリミティブ型(数値、文字列、真偽値、null, undefined, Symbol, BigInt)の比較に使います。オブジェクトや配列には通常使いません(参照が同じであるかを見ます)。
javascript
test('toBe usage', () => {
expect(1 + 1).toBe(2);
expect('hello').toBe('hello');
expect(true).toBe(true);
});-
.toEqual(value)
: 値の再帰的な比較を行います。オブジェクトや配列の内容が同じであるかをチェックするのに使います。オブジェクトのプロパティや配列の要素が全て等しい場合に真となります。
“`javascript
test(‘toEqual usage’, () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
expect(obj1).toEqual(obj2); // オブジェクトの内容が同じなのでPASSconst arr1 = [1, 2, { a: 3 }];
const arr2 = [1, 2, { a: 3 }];
expect(arr1).toEqual(arr2); // 配列の内容が同じなのでPASSexpect({ a: 1 }).not.toBe({ a: 1 }); // 参照が異なるのでPASS
expect({ a: 1 }).toEqual({ a: 1 }); // 値が同じなのでPASS
});
* **`.not`**: どのマッチャーとも組み合わせて、否定的な条件をチェックします。例えば `expect(...).not.toBe(...)` は、「〜ではないこと」を期待するという意味になります。
javascript
test(‘not usage’, () => {
expect(1 + 1).not.toBe(3);
expect(‘hello’).not.toBe(‘world’);
expect({ a: 1 }).not.toBe({ a: 1 }); // 参照が異なるのでPASS
expect({ a: 1 }).not.toEqual({ b: 2 }); // 値が異なるのでPASS
});
“`
真偽値のマッチャー
特定の真偽値や null/undefined をチェックするためのマッチャーです。
.toBeNull()
: 値がnull
であることをチェックします。.toBeUndefined()
: 値がundefined
であることをチェックします。.toBeDefined()
: 値がundefined
でないことをチェックします。(toBeUndefined().not
と同じ).toBeTruthy()
: 値が真(truthy value)であることをチェックします。JavaScriptのif
文などでtrue
と評価される値(例えば、非ゼロの数値、空でない文字列、オブジェクト、配列など)です。.toBeFalsy()
: 値が偽(falsy value)であることをチェックします。JavaScriptのif
文などでfalse
と評価される値(false
,0
,''
,null
,undefined
,NaN
)です。
“`javascript
test(‘boolean and null/undefined matchers’, () => {
const n = null;
const u = undefined;
const zero = 0;
const emptyString = ”;
const value = ‘hello’;
const obj = {};
expect(n).toBeNull();
expect(u).toBeUndefined();
expect(value).toBeDefined();
expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect(‘ ‘).toBeTruthy();
expect([]).toBeTruthy();
expect({}).toBeTruthy();
expect(false).toBeFalsy();
expect(zero).toBeFalsy();
expect(emptyString).toBeFalsy();
expect(n).toBeFalsy();
expect(u).toBeFalsy();
expect(NaN).toBeFalsy();
});
“`
数値のマッチャー
数値の比較に特化したマッチャーです。浮動小数点数の比較には注意が必要です。
.toBeGreaterThan(number)
: より大きい.toBeGreaterThanOrEqual(number)
: 以上.toBeLessThan(number)
: より小さい.toBeLessThanOrEqual(number)
: 以下
javascript
test('numeric matchers', () => {
const value = 10;
expect(value).toBeGreaterThan(8);
expect(value).toBeGreaterThanOrEqual(10);
expect(value).toBeLessThan(12);
expect(value).toBeLessThanOrEqual(10);
});.toBeCloseTo(number, numDigits?)
: 浮動小数点数の近似値を比較します。小数点以下の桁数を指定できます。
javascript
test('toBeCloseTo usage', () => {
const floatingPoint = 0.1 + 0.2;
// expect(floatingPoint).toBe(0.3); // これは失敗する(浮動小数点演算の誤差)
expect(floatingPoint).toBeCloseTo(0.3); // PASS
expect(0.12345).toBeCloseTo(0.123, 3); // 小数点以下3桁まで比較
expect(0.12345).not.toBeCloseTo(0.123, 4); // 小数点以下4桁では一致しないのでPASS
});
文字列のマッチャー
文字列のパターンマッチングに使います。
.toMatch(regexpOrString)
: 文字列が正規表現にマッチするか、または部分文字列を含むかをチェックします。
javascript
test('toMatch usage', () => {
expect('Hello World').toMatch(/World/); // 正規表現でマッチ
expect('Hello World').toMatch('World'); // 部分文字列を含むか
expect('Testing Jest').not.toMatch(/React/);
});
配列・イテラブルのマッチャー
配列やその他のイテラブル(反復可能なオブジェクト)の要素に関するマッチャーです。
-
.toContain(item)
: 配列やイテラブルに特定の要素が含まれているかをチェックします。プリミティブ型とオブジェクトで挙動が異なります(オブジェクトの場合は参照で比較)。
“`javascript
test(‘toContain usage’, () => {
const shoppingList = [
‘diapers’,
‘kleenex’,
‘trash bags’,
‘paper towels’,
‘milk’,
];
expect(shoppingList).toContain(‘milk’);
expect(shoppingList).not.toContain(‘beer’);const arrOfObjects = [{ id: 1, name: ‘A’ }, { id: 2, name: ‘B’ }];
// expect(arrOfObjects).toContain({ id: 1, name: ‘A’ }); // 通常これは失敗する(参照が異なるため)
// オブジェクトの内容でチェックしたい場合は、any または objectContaining と組み合わせるか、toEqual を使う
expect(arrOfObjects).toEqual(expect.arrayContaining([{ id: 1, name: ‘A’ }])); // arrayContaining と組み合わせる
});
* **`.toHaveLength(number)`**: 配列や文字列など、`length` プロパティを持つものの長さをチェックします。
javascript
test(‘toHaveLength usage’, () => {
expect([1, 2, 3]).toHaveLength(3);
expect(‘hello’).toHaveLength(5);
});
“`
オブジェクトのマッチャー
オブジェクトのプロパティに関するマッチャーです。
.toHaveProperty(keyPath, value?)
: オブジェクトが特定のプロパティを持っているかをチェックします。ネストしたプロパティもドット記法 ('a.b.c'
) で指定できます。省略可能な2番目の引数で、そのプロパティの値までチェックできます。
javascript
test('toHaveProperty usage', () => {
const user = {
id: 1,
name: 'Alice',
address: {
city: 'Tokyo',
zip: '100-0001'
}
};
expect(user).toHaveProperty('name'); // 'name' プロパティがあるか
expect(user).toHaveProperty('name', 'Alice'); // 'name' プロパティがあり、値が 'Alice' か
expect(user).toHaveProperty('address.city'); // ネストしたプロパティがあるか
expect(user).toHaveProperty('address.zip', '100-0001'); // ネストしたプロパティがあり、値も一致するか
expect(user).not.toHaveProperty('age');
});-
.toMatchObject(object)
: オブジェクトが、指定したオブジェクトのプロパティを「全て」持っており、かつその値が等しいかをチェックします。指定していないプロパティは無視されます。部分的なオブジェクトの一致をチェックするのに便利です。.toEqual
はオブジェクト全体が完全に一致する必要があるのに対し、.toMatchObject
は部分的な一致を許容します。
“`javascript
test(‘toMatchObject usage’, () => {
const user = {
id: 1,
name: ‘Alice’,
age: 30,
address: {
city: ‘Tokyo’,
zip: ‘100-0001’
}
};
// user オブジェクトが少なくとも { name: ‘Alice’, age: 30 } を満たすか
expect(user).toMatchObject({ name: ‘Alice’, age: 30 });// ネストしたオブジェクトもチェック可能
expect(user).toMatchObject({ address: { city: ‘Tokyo’ } });// 存在しないプロパティや値が異なる場合は失敗
expect(user).not.toMatchObject({ occupation: ‘Engineer’ });
expect(user).not.toMatchObject({ name: ‘Bob’ });// 配列内のオブジェクトに対しても使える (toEqual + arrayContaining と組み合わせるなど)
const users = [
{ id: 1, name: ‘Alice’ },
{ id: 2, name: ‘Bob’ }
];
expect(users).toEqual(expect.arrayContaining([
expect.objectContaining({ name: ‘Alice’ })
]));
});
``
expect.objectContainingは、
toMatchObjectの
expect内でのバージョンと考えると分かりやすいです。
.arrayContaining` と組み合わせて、配列の中に「特定のプロパティセットを持つオブジェクト」が含まれているかをチェックする際によく使われます。
例外のマッチャー
特定の処理がエラー(例外)を発生させることをテストします。
-
.toThrow(error?)
: 実行される関数が例外をスローすることをチェックします。特定の例外メッセージや例外クラスを指定することもできます。テスト対象のコードは、引数として関数参照を渡す必要があります(例えば、アロー関数で囲む)。
“`javascript
function compileAndroidCode() {
throw new Error(‘you are using the wrong JDK’);
}function unsafeOperation() {
// 何か危険な処理
}test(‘toThrow usage’, () => {
// 関数がエラーをスローすることを期待
expect(() => compileAndroidCode()).toThrow();// 特定のエラーメッセージを期待(文字列または正規表現)
expect(() => compileAndroidCode()).toThrow(‘you are using the wrong JDK’);
expect(() => compileAndroidCode()).toThrow(/JDK/);// 特定のエラーオブジェクト(クラス)を期待
// expect(() => compileAndroidCode()).toThrow(Error); // この例ではこれもPASSする// エラーをスローしないことを期待
expect(() => unsafeOperation()).not.toThrow();
});
“`
その他の便利なマッチャー(any, arrayContainingなど)
Jestには、より柔軟な検証を可能にするユーティリティマッチャーも用意されています。これらは expect.any()
, expect.stringContaining()
のように expect
の静的プロパティとして使用します。
expect.any(constructor)
: 値が指定されたコンストラクタ(例:Number
,String
,Array
,Object
,Function
,Error
など)によって作成されたインスタンスであることをチェックします。型のチェックに使えます。
javascript
test('expect.any usage', () => {
expect(123).toEqual(expect.any(Number));
expect('abc').toEqual(expect.any(String));
expect([1, 2]).toEqual(expect.any(Array));
expect({}).toEqual(expect.any(Object));
expect(() => {}).toEqual(expect.any(Function));
});expect.anything()
: 値がnull
またはundefined
でないことをチェックします。
javascript
test('expect.anything usage', () => {
expect(123).toEqual(expect.anything());
expect('abc').toEqual(expect.anything());
expect(null).not.toEqual(expect.anything());
expect(undefined).not.toEqual(expect.anything());
});-
expect.arrayContaining(array)
: 配列が、指定された配列の要素を「全て」含んでいることをチェックします。要素の順序や、指定されていない他の要素の存在は問いません。
“`javascript
test(‘expect.arrayContaining usage’, () => {
const expected = [‘Alice’, ‘Bob’];
const received = [‘Charlie’, ‘Alice’, ‘David’, ‘Bob’];
expect(received).toEqual(expect.arrayContaining(expected)); // receivedがexpectedの要素を全て含んでいるのでPASSconst expectedPartial = [{ id: 1, name: ‘Alice’ }];
const receivedObjects = [
{ id: 1, name: ‘Alice’, age: 30 },
{ id: 2, name: ‘Bob’, age: 25 }
];
// オブジェクトの内容で比較したい場合は objectContaining と組み合わせる
expect(receivedObjects).toEqual(expect.arrayContaining([
expect.objectContaining({ name: ‘Alice’ })
]));
});
* **`expect.objectContaining(object)`**: オブジェクトが、指定されたオブジェクトのプロパティを「全て」持っており、かつその値が等しいかをチェックします。`.toMatchObject` と同じ用途ですが、こちらは他のマッチャー(例: `expect.arrayContaining`) と組み合わせて使う場合に便利です。
javascript
test(‘expect.objectContaining usage’, () => {
const received = { a: 1, b: 2, c: 3 };
expect(received).toEqual(expect.objectContaining({ a: 1, c: 3 })); // receivedが { a: 1, c: 3 } を満たすのでPASS
});
``
expect.stringContaining(string)
* ****: 文字列が指定された部分文字列を含んでいるかをチェックします。
.toMatch(‘substring’)と同じ用途です。
expect.stringMatching(regexp)
* ****: 文字列が指定された正規表現にマッチするかをチェックします。
.toMatch(/regexp/)` と同じ用途です。
これらのマッチャーを組み合わせることで、様々な複雑な条件を表現し、コードの挙動を正確に検証することができます。どのマッチャーを使えば良いか迷ったら、Jestの公式ドキュメントを参照すると良いでしょう。
Setup and Teardown (セットアップとティアダウン):テスト前後の準備とクリーンアップ
複数のテストケースを実行する際に、共通の準備処理(セットアップ)や後処理(ティアダウン)が必要になることがあります。例えば、データベースへの接続、一時ファイルの作成、モックの設定などが考えられます。Jestは、これらの処理を効率的に行うためのヘルパー関数を提供しています。
これらの関数は、現在の describe
ブロック、またはトップレベルに定義された describe
ブロック全体に適用されます。
beforeEach(fn)
: 現在のスコープ内の「各テストケース」が実行される 直前 に指定した関数fn
を実行します。afterEach(fn)
: 現在のスコープ内の「各テストケース」が実行された 直後 に指定した関数fn
を実行します。beforeAll(fn)
: 現在のスコープ内の「全てのテストケース」が実行される 最初 に指定した関数fn
を1回だけ実行します。afterAll(fn)
: 現在のスコープ内の「全てのテストケース」が実行された 最後 に指定した関数fn
を1回だけ実行します。
これらの関数も非同期処理に対応しています(done
や async/await
を使います。詳細は非同期テストのセクションで説明します)。
例として、各テストで共有するデータを準備し、各テスト後にそのデータをクリアするようなシナリオを考えてみましょう。
“`javascript
// テスト対象の簡単なクラス
class Database {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
}
getUser(id) {
return this.users.find(user => user.id === id);
}
clear() {
this.users = [];
}
}
describe(‘Database Operations’, () => {
let db; // 各テストケースで新しいDatabaseインスタンスを使う
// 各テストケースの直前に実行される
beforeEach(() => {
console.log(‘ beforeEach: Creating new DB instance’);
db = new Database(); // 新しいインスタンスを生成
});
// 各テストケースの直後に実行される
afterEach(() => {
console.log(‘ afterEach: Clearing DB’);
db.clear(); // インスタンスをクリア
});
// describeブロック全体で一度だけ実行される(例えばDB接続など)
// beforeAll(() => {
// console.log(‘beforeAll: Connecting to DB’);
// // DB接続処理など…
// });
// describeブロック全体が終了した後に一度だけ実行される(例えばDB切断など)
// afterAll(() => {
// console.log(‘afterAll: Disconnecting from DB’);
// // DB切断処理など…
// });
test(‘should add a user to the database’, () => {
console.log(‘ Running test: add user’);
const user = { id: 1, name: ‘Alice’ };
db.addUser(user);
expect(db.getUser(1)).toEqual(user);
expect(db.users).toHaveLength(1);
});
test(‘should retrieve a user by id’, () => {
console.log(‘ Running test: retrieve user’);
const user1 = { id: 1, name: ‘Alice’ };
const user2 = { id: 2, name: ‘Bob’ };
db.addUser(user1);
db.addUser(user2);
expect(db.getUser(1)).toEqual(user1);
expect(db.getUser(2)).toEqual(user2);
expect(db.users).toHaveLength(2);
});
test(‘should return undefined if user not found’, () => {
console.log(‘ Running test: user not found’);
expect(db.getUser(99)).toBeUndefined();
expect(db.users).toHaveLength(0); // beforeEachでクリアされていることを確認
});
});
“`
この例のように、beforeEach
を使うことで、各テストケースが独立した環境(ここでは空のデータベースインスタンス)で実行されることが保証されます。これにより、あるテストケースの実行結果が他のテストケースに影響を与える(テスト間の依存関係が発生する)ことを防ぎ、テストの信頼性を高めることができます。
beforeAll
と afterAll
は、例えばテストスイート全体で一度だけ必要なセットアップ(例:テスト用のDBコンテナ起動、テストデータの投入)やティアダウン(例:DBコンテナ停止、テストデータ削除)に適しています。
describe
ブロックの中でこれらの関数を定義すると、その describe
ブロックとその子 describe
ブロック内のテストにのみ適用されます。これにより、より細かいスコープでセットアップ/ティアダウンを管理できます。
Mock Functions (モック関数):依存関係の排除
実際のアプリケーションでは、関数やモジュールは他の関数や外部サービス(API通信、データベースアクセスなど)に依存していることがよくあります。これらの依存関係があるままテストを実行すると、以下のような問題が発生することがあります。
- テストが遅くなる: 外部サービスへのアクセスは時間がかかることがあります。
- テストが不安定になる: 外部サービスの状態によってテスト結果が変わってしまう可能性があります。
- テストが困難になる: データベースの状態を特定の状態にするのが難しい、APIのレスポンスをコントロールできない、といった問題があります。
- テストのスコープが広がる: ユニットテストなのに、依存先のコードやシステムも間接的にテストしてしまうことになります。ユニットテストは「単一のユニット(関数、クラスなど)」の振る舞いをテストすべきです。
これらの問題を解決するために、「モック」(Mock)という手法を使います。モックとは、依存する関数やモジュールを「偽物」や「代替」に置き換えることです。この偽物は、テストに必要な振る舞い(特定の引数に対して特定の値を返す、指定された回数呼び出されたか記録するなど)だけを定義できます。
Jestはモック機能を強力にサポートしています。
jest.fn()
で関数をモック
最も基本的なモックの作り方は、jest.fn()
を使うことです。これは、呼び出し状況(引数、回数、返り値、エラーなど)を記録する「モック関数」(または「スパイ関数」とも呼ばれます)を作成します。
“`javascript
test(‘mock function usage’, () => {
// jest.fn() でモック関数を作成
const mockFunction = jest.fn();
// モック関数を呼び出す
mockFunction(‘hello’, 123);
mockFunction(‘world’);
// モック関数が呼び出されたことを検証するマッチャー
expect(mockFunction).toHaveBeenCalled(); // 少なくとも1回呼び出されたか
expect(mockFunction).toHaveBeenCalledTimes(2); // 呼び出された回数
expect(mockFunction).toHaveBeenCalledWith(‘hello’, 123); // 特定の引数で呼び出されたか
expect(mockFunction).toHaveBeenLastCalledWith(‘world’); // 最後に呼び出された時の引数
});
“`
jest.fn()
で作成したモック関数には、mock
というプロパティがあり、そこから呼び出しに関する詳細情報(calls
, results
, instances
など)にアクセスできます。
“`javascript
test(‘mock function properties’, () => {
const mockFunction = jest.fn();
mockFunction(‘arg1’, ‘arg2’);
mockFunction(1, 2);
// mock.calls: 呼び出しごとに引数の配列を記録した配列
expect(mockFunction.mock.calls).toEqual([
[‘arg1’, ‘arg2’],
[1, 2]
]);
// mock.results: 呼び出しごとの結果を記録した配列(typeとvalue)
expect(mockFunction.mock.results[0].type).toBe(‘return’);
// expect(mockFunction.mock.results[0].value).toBe(undefined); // デフォルトの返り値
// mock.instances: 呼び出しごとに this の値を記録した配列(コンストラクタとして呼び出された場合など)
});
“`
モック関数の動作定義
デフォルトでは、jest.fn()
で作成したモック関数は何もしません(返り値は undefined
)。しかし、テストのシナリオに合わせて、特定の値を返したり、特定の実装を実行させたりすることができます。
.mockReturnValue(value)
: 呼び出された際に常に指定したvalue
を返します。
javascript
test('mockReturnValue usage', () => {
const mockFn = jest.fn().mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn()).toBe(42);
});-
.mockReturnValueOnce(value)
: 呼び出された際に、その呼び出し「一度だけ」指定したvalue
を返します。複数回指定すると、呼び出されるたびにキューから値が消費されます。キューが尽きると、デフォルトの動作(undefined
または.mockReturnValue
で指定した値)に戻ります。
“`javascript
test(‘mockReturnValueOnce usage’, () => {
const mockFn = jest.fn()
.mockReturnValueOnce(10)
.mockReturnValueOnce(20)
.mockReturnValue(30); // キューが尽きた後のデフォルトexpect(mockFn()).toBe(10); // 1回目の呼び出し
expect(mockFn()).toBe(20); // 2回目の呼び出し
expect(mockFn()).toBe(30); // 3回目の呼び出し(キューが尽きた)
expect(mockFn()).toBe(30); // 4回目以降
});
* **`.mockImplementation(fn)`**: モック関数の「実装」を、指定した関数 `fn` に置き換えます。実際の処理内容をモック関数に持たせたい場合に便利です。
javascript
test(‘mockImplementation usage’, () => {
const mockFn = jest.fn((a, b) => a + b); // 足し算をする実装をモックに持たせる
expect(mockFn(1, 2)).toBe(3);
expect(mockFn).toHaveBeenCalledWith(1, 2);
});
* **`.mockImplementationOnce(fn)`**: `.mockImplementation` の「一度だけ」バージョンです。
javascript
test(‘mockImplementationOnce usage’, () => {
const mockFn = jest.fn()
.mockImplementationOnce((a, b) => a * b) // 1回目は掛け算
.mockImplementationOnce((a, b) => a / b); // 2回目は割り算expect(mockFn(2, 3)).toBe(6); // 1回目の呼び出し
expect(mockFn(6, 3)).toBe(2); // 2回目の呼び出し
// 3回目以降はデフォルト動作(undefined)または mockImplementation で指定した動作
});
* **`.mockResolvedValue(value)`**: 非同期モック関数の Promise が解決された際に、指定した `value` で解決されるように定義します。
javascript
test(‘mockResolvedValue usage’, async () => {
const asyncMock = jest.fn().mockResolvedValue(‘resolved value’);
await expect(asyncMock()).resolves.toBe(‘resolved value’);
expect(asyncMock).toHaveBeenCalled();
});
* **`.mockRejectedValue(error)`**: 非同期モック関数の Promise が拒否された際に、指定した `error` で拒否されるように定義します。
javascript
test(‘mockRejectedValue usage’, async () => {
const asyncMock = jest.fn().mockRejectedValue(new Error(‘async error’));
await expect(asyncMock()).rejects.toThrow(‘async error’);
expect(asyncMock).toHaveBeenCalled();
});
“`
Spy (スパイ):既存の関数を監視
モックは依存する関数を「置き換える」のに対し、スパイは「既存の関数を呼び出しつつ、その呼び出し状況を監視する」ためのものです。Jestでは jest.spyOn()
を使います。
jest.spyOn(object, methodName)
は、object
の methodName
メソッドに対してスパイを設定します。これにより、そのメソッドが呼び出された際に、元の実装も実行されつつ、jest.fn()
で作成したモック関数と同様の呼び出し記録が行われます。
“`javascript
const calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a – b,
};
test(‘jest.spyOn usage’, () => {
// calculator.add メソッドにスパイを設定
const addSpy = jest.spyOn(calculator, ‘add’);
// 元のメソッドを呼び出す
const result = calculator.add(1, 2);
// スパイを使って呼び出し状況を検証
expect(result).toBe(3); // 元のメソッドは実行される
expect(addSpy).toHaveBeenCalled();
expect(addSpy).toHaveBeenCalledTimes(1);
expect(addSpy).toHaveBeenCalledWith(1, 2);
// スパイを解除する(テスト後にクリーンアップすることが重要)
addSpy.mockRestore(); // または afterEach などで spy.mockClear() または spy.mockReset()
});
“`
jest.spyOn
で作成したスパイは、jest.fn()
と同様に .mockReturnValue
, .mockImplementation
などのメソッドを持っています。これらを使うと、一時的に元の実装を置き換えることも可能です。
スパイは、特に既存のコードベースにテストを追加する際に便利です。元の実装を変更せずに、そのメソッドが正しく呼び出されているかを確認できます。
Mock Modules (モックモジュール):外部モジュール全体をモック
アプリケーションは、しばしばファイルシステム操作 (fs
)、ネットワーク通信 (axios
, fetch
)、データベースライブラリなどの外部モジュールに依存します。これらのモジュールを実際に呼び出すと、上記で説明したようなテストの遅延や不安定さの原因となります。
Jestでは、jest.mock()
を使ってモジュール全体をモックすることができます。
jest.mock('module-name')
: 指定したモジュール全体を自動的にモックします。そのモジュールのエクスポートは全て Jest のモック関数に置き換えられます。jest.mock('module-name', factory)
: 指定したモジュール全体を、指定したファクトリー関数が返す値でモックします。より詳細なモックの振る舞いを定義できます。
例として、ユーザーデータを取得するために axios
ライブラリを使う関数をテストする場合を考えます。
“`javascript
// src/api.js
const axios = require(‘axios’);
async function getUser(id) {
const response = await axios.get(/users/${id}
);
return response.data;
}
module.exports = { getUser };
“`
この getUser
関数をテストする際に、実際に外部APIを呼び出したくありません。そこで axios
をモックします。
“`javascript
// src/api.test.js
const { getUser } = require(‘./api’);
const axios = require(‘axios’);
// axios モジュール全体をモックする
jest.mock(‘axios’); // これにより、このテストファイル内で require(‘axios’) が
// Jestによって生成されたモックオブジェクトを返すようになる
test(‘getUser fetches user data’, async () => {
const userId = 1;
const mockUserData = { id: 1, name: ‘Alice’ };
// axios.get メソッドのモック実装を定義
// mockResolvedValue を使うと、その呼び出しが指定した値で解決されるPromiseを返すようになる
axios.get.mockResolvedValue({ data: mockUserData });
// テスト対象の関数を実行
const data = await getUser(userId);
// axios.get が正しいURLで呼び出されたか検証
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(/users/${userId}
);
// getUser が正しいデータを返したか検証
expect(data).toEqual(mockUserData);
// テスト後、モックの状態をクリアすることが推奨される
// axios.get.mockClear(); // または afterEach で jest.clearAllMocks()
});
“`
jest.mock('axios');
をテストファイルの先頭(import
や require
の後)に記述することで、そのテストファイル全体で axios
がモックされます。そして、モックされた axios
オブジェクトのメソッド(例: axios.get
)に対して、mockResolvedValue
などを使って具体的な振る舞いを定義します。
手動モック (__mocks__
ディレクトリ)
jest.mock('module-name')
は、モジュール全体を自動的にモックし、全てのエクスポートを jest.fn()
に置き換えます。しかし、モックされたモジュールにもっと複雑な振る舞いをさせたい場合があります。そんなときは、「手動モック」を使います。
手動モックは、モックしたいモジュールと同じ階層に __mocks__
というディレクトリを作成し、その中にモジュールと同じ名前のファイルを作成します。
例えば、src/utils.js
というファイルをモックしたい場合、src/__mocks__/utils.js
というファイルを作成します。
javascript
// src/utils.js
module.exports = {
fetchData: async (url) => { /* ... 実際のデータ取得処理 ... */ },
formatData: (data) => { /* ... 実際のデータ整形処理 ... */ },
};
javascript
// src/__mocks__/utils.js
// 手動モックファイルでは、モックしたい振る舞いをエクスポートする
module.exports = {
// fetchData をモック関数に置き換える
fetchData: jest.fn(),
// formatData は元の実装をそのまま使う(または別のモック実装にする)
formatData: jest.fn(data => `Mocked: ${JSON.stringify(data)}`),
};
そして、テストファイルでは jest.mock('./utils');
と記述します。Jestはこれを見ると、自動生成されたモックではなく src/__mocks__/utils.js
の内容を使ってモックを行います。
“`javascript
// src/some-module.js (テスト対象のコード)
const utils = require(‘./utils’);
async function processData(url) {
const rawData = await utils.fetchData(url);
return utils.formatData(rawData);
}
module.exports = { processData };
“`
“`javascript
// src/some-module.test.js
const { processData } = require(‘./some-module’);
const utils = require(‘./utils’); // 手動モックした utils が require される
// utils モジュールをモックする(mocks ディレクトリを見る)
jest.mock(‘./utils’);
// utils.fetchData がモック関数であることを確認
const mockedFetchData = utils.fetchData;
const mockedFormatData = utils.formatData;
test(‘processData uses mocked utils’, async () => {
const testUrl = ‘http://example.com/data’;
const rawData = { key: ‘value’ };
const formattedData = ‘Mocked: {“key”:”value”}’;
// モック関数の振る舞いを定義
mockedFetchData.mockResolvedValue(rawData);
// mockedFormatData は手動モックファイルで実装済み
// テスト対象関数を実行
const result = await processData(testUrl);
// モックが正しく呼び出されたか検証
expect(mockedFetchData).toHaveBeenCalledTimes(1);
expect(mockedFetchData).toHaveBeenCalledWith(testUrl);
expect(mockedFormatData).toHaveBeenCalledTimes(1);
expect(mockedFormatData).toHaveBeenCalledWith(rawData);
// 結果が正しいか検証
expect(result).toBe(formattedData);
});
“`
手動モックは、特定のモックの再利用性が高い場合や、モックに複雑なロジックを含めたい場合に適しています。
モック機能はJestの強力な点であり、ユニットテストを効果的に行う上で不可欠です。依存関係を排除し、テスト対象のユニット単体の振る舞いに集中できるようになります。
Snapshot Testing (スナップショットテスト):UIなどの変更を検出
スナップショットテストは、UIコンポーネントや設定オブジェクト、APIレスポンスなどの、ある時点での「状態」をファイルに保存しておき、その後のテスト実行時に現在の状態が保存されたスナップショットと一致するかを比較するテスト手法です。
これは、以下のような場合に非常に有効です。
- UIコンポーネント: ReactやVueなどのコンポーネントのレンダリング結果(仮想DOM構造)が意図せず変更されていないかを確認する。
- 設定オブジェクト: 生成される設定オブジェクトの構造や値が期待通りか確認する。
- APIレスポンス: 実際に取得したAPIレスポンスのデータ構造が期待通りか確認する(ただし、データの内容自体が変わりうる場合は注意が必要)。
スナップショットテストは、コードの「出力」や「見た目」(構造)が不変であることを保証するのに役立ちますが、その出力の内容が「正しいか」を論理的に検証するものではないという点に注意が必要です。あくまで「以前の状態から変更がないか」を検出するものです。
使い方 (toMatchSnapshot
)
スナップショットテストは、expect().toMatchSnapshot()
マッチャーを使って行います。
例として、Reactコンポーネント(もちろんJestはReact専用ではありませんが、UIテストの例としてよく使われます)のスナップショットテストを考えます。Reactをテストするには @testing-library/react
や react-test-renderer
といったライブラリと組み合わせることが一般的です。ここでは簡単な仮想DOM構造を返す関数をテストします。
“`javascript
// src/components.js
function Header({ title }) {
return {
type: ‘h1’,
props: { className: ‘header’ },
children: title
};
}
function Button({ onClick, text }) {
return {
type: ‘button’,
props: { onClick: onClick },
children: text
};
}
module.exports = { Header, Button };
“`
“`javascript
// src/components.test.js
const { Header, Button } = require(‘./components’);
describe(‘Component Snapshots’, () => {
test(‘Header should render correctly’, () => {
const headerComponent = Header({ title: ‘Hello, Jest!’ });
// 最初の実行時にスナップショットファイルが生成される
// 2回目以降は、生成された構造とスナップショットを比較
expect(headerComponent).toMatchSnapshot();
});
test(‘Button should render correctly’, () => {
const buttonComponent = Button({ onClick: () => {}, text: ‘Click Me’ });
expect(buttonComponent).toMatchSnapshot();
});
});
“`
このテストを初めて実行すると、テストファイルと同じディレクトリに __snapshots__
というディレクトリが作成され、その中にテストファイル名に .snap
が付加されたファイル (src/__snapshots__/components.test.js.snap
) が生成されます。
src/__snapshots__/components.test.js.snap
の内容は以下のようになるはずです(バージョンなどにより多少異なる場合があります)。
“`javascript
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[Component Snapshots Button should render correctly 1
] = {
;
"children": "Click Me",
"props": {
"onClick": [Function],
},
"type": "button",
}
exports[Component Snapshots Header should render correctly 1
] = {
;
"children": "Hello, Jest!",
"props": {
"className": "header",
},
"type": "h1",
}
“`
これは、Jestが expect().toMatchSnapshot()
が呼び出された時点の値をシリアライズして保存したものです。
次にテストを実行すると、Jestは現在の Header({ title: 'Hello, Jest!' })
や Button(...)
の出力構造と、保存されているスナップショットを比較します。
もしコードを変更して、例えば Header
のクラス名を 'header'
から 'page-title'
に変更した場合、次回のテスト実行時にスナップショットとの差異が検出され、テストは失敗します。
javascript
// src/components.js (変更後)
function Header({ title }) {
return {
type: 'h1',
props: { className: 'page-title' }, // 変更
children: title
};
}
// ... Button は変更なし
module.exports = { Header, Button };
npm test
を実行すると、以下のような出力が表示され、スナップショットの不一致がレポートされます。
“`
FAIL src/components.test.js
Component Snapshots
✕ Header should render correctly (4ms)
✓ Button should render correctly (1ms)
● Component Snapshots › Header should render correctly
Snapshot Diff:
- Snapshot
+ Received
@@ -1,7 +1,7 @@
{
"children": "Hello, Jest!",
"props": {
- "className": "header",
+ "className": "page-title", // ★ここが違うとレポートされる
},
"type": "h1",
}
… 中略 …
Snapshots: 1 failed, 1 passed, 2 total
Tests: 1 failed, 1 passed, 2 total
“`
変更が意図したものであれば、スナップショットを更新する必要があります。 Jestの実行時に -u
オプションをつけることで、スナップショットを現在の状態に更新できます。
bash
npm test -- -u
または
bash
yarn test -u
これにより、src/__snapshots__/components.test.js.snap
ファイルが新しい出力で上書きされ、次回のテストからはその新しいスナップショットと比較されるようになります。
スナップショットテストのメリット・デメリット
メリット:
- 高速: UI全体を手動で検査するよりもはるかに高速です。
- 網羅性: オブジェクト全体の構造を一度に捉えるため、細かいプロパティの変更も見逃しにくいです。
- 開発効率: 「この変更で何かが壊れていないか」を手軽にチェックできます。
デメリット:
- 「正しいか」の検証ではない: スナップショットは「以前と同じか」を保証するだけで、その内容がビジネスロジック的に「正しい」かを検証するものではありません。あくまで意図しない変更の検出に特化しています。
- 意図した変更時の手間: コードを変更するたびにスナップショットの更新が必要になる場合があります。これが頻繁すぎると煩雑に感じることもあります。
- レビューの手間: スナップショットファイル自体もコードリポジトリにコミットされるため、プルリクエストのレビュー時には
.snap
ファイルの変更内容も確認する必要があります。意図した変更かどうかを慎重に確認しないと、誤ってバグのあるスナップショットを承認してしまう可能性があります。
スナップショットテストは、UIコンポーネントのレンダリング結果や、APIレスポンスの構造確認など、特定の用途で非常に強力なツールとなります。他のユニットテストや統合テストと組み合わせて使うことで、テスト戦略全体の効果を高めることができます。
非同期テスト:Promise, async/await, コールバック
JavaScriptでは、ネットワーク通信、ファイル操作、タイマーなど、非同期処理が頻繁に使われます。Jestは非同期処理のテストも適切にサポートしています。
非同期テストの基本的な考え方は、「非同期処理が完了し、期待する状態になったことを Jest に知らせる」ことです。Jestは、デフォルトではテストケース内のコードが全て同期的に実行された後、すぐにテストを終了します。そのため、非同期処理が完了する前にテストが終わってしまい、「非同期処理の結果を検証するコード」が実行されないままテストが成功したと誤認識される可能性があります。
Jestで非同期テストを行う主な方法は以下の3つです。
done
コールバック: コールバックベースの非同期処理や、Promiseが使えない古いAPIのテストに使います。テスト関数がdone
という引数を受け取り、非同期処理が完了した後にdone()
を呼び出すことで、Jestにテストの終了を知らせます。- Promise を返す: テスト関数が Promise を返す場合、Jestはその Promise が解決(resolve)または拒否(reject)されるまでテストの終了を待ちます。
async/await
: Promise ベースの非同期処理を、同期的なコードのように記述できるasync/await
構文もJestは完全にサポートしています。
done
コールバック
古いAPIや、Promiseを返さないコールバック形式のAPIをテストする場合に使います。
“`javascript
// テスト対象の架空の非同期関数(コールバック形式)
function fetchDataWithCallback(callback) {
setTimeout(() => {
callback(‘peanut butter’);
}, 1000); // 1秒後にコールバックを実行
}
test(‘the data is peanut butter (with done)’, (done) => {
function callback(data) {
try {
expect(data).toBe(‘peanut butter’);
done(); // ★ 非同期処理と検証が完了したら done() を呼ぶ
} catch (error) {
done(error); // エラーが発生した場合は done(error) を呼ぶ
}
}
fetchDataWithCallback(callback);
// done() が呼ばれるまで、Jestはテストの終了を待つ
});
// done() が呼ばれずにテスト関数が終了した場合、テストはタイムアウトエラーとなる
“`
done
を使う場合、done()
を呼び忘れるとテストが終了せずタイムアウトエラーになります。また、非同期処理中にエラーが発生した場合に done(error)
を呼ばないと、Jestはそのエラーを検知できない場合があります。
Promise を返す
テスト関数が Promise を返す場合、Jestはその Promise の状態(解決または拒否)を監視します。
“`javascript
// テスト対象の架空の非同期関数(Promise形式)
function fetchDataWithPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(‘lemonade’); // 成功する場合
reject(‘error’); // 失敗する場合
}, 1000);
});
}
test(‘the data is lemonade (with Promise)’, () => {
// test 関数が Promise を返すようにする
return fetchDataWithPromise().then(data => {
// Promise が解決された後の検証
expect(data).toBe(‘lemonade’);
});
});
test(‘the fetch fails with an error (with Promise)’, () => {
expect.assertions(1); // reject を検証する場合、expect() が最低1回呼ばれることを保証するために使うことがある
return fetchDataWithPromise().catch(e => {
// Promise が拒否された後の検証
expect(e).toMatch(‘error’);
});
});
“`
Promise を返す方法では、done
を呼び出す手間が省けます。Promise が解決すればテスト成功、拒否されればテスト失敗となります。
async/await
Promise ベースの非同期処理をテストする最も一般的で推奨される方法は、async/await
を使うことです。テスト関数を async
キーワードでマークし、非同期処理の前に await
をつけます。
“`javascript
// テスト対象の架空の非同期関数(Promise形式)
function fetchDataWithPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(‘orange juice’); // 成功する場合
reject(‘error’); // 失敗する場合
}, 1000);
});
}
test(‘the data is orange juice (with async/await)’, async () => {
// async キーワードをつける
const data = await fetchDataWithPromise(); // await で Promise の解決を待つ
expect(data).toBe(‘orange juice’); // 同期コードのように検証
});
test(‘the fetch fails with an error (with async/await)’, async () => {
expect.assertions(1);
try {
await fetchDataWithPromise();
} catch (e) {
// try-catch ブロックでエラーを捕まえて検証
expect(e).toMatch(‘error’);
}
});
“`
async/await
を使うと、非同期テストコードが非常に読みやすく、同期的なコードのように書けるため、最も推奨されるスタイルです。
.resolves
と .rejects
マッチャー
非同期テストにおいて、Promise が特定の値で解決されるか、または特定のエラーで拒否されるかをより簡潔に検証するために、.resolves
および .rejects
マッチャーを使うことができます。これらは await expect(promise)
と組み合わせて使用します。
“`javascript
// テスト対象の架空の非同期関数(Promise形式)
function fetchDataWithPromise(shouldResolve) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve(‘grape’);
} else {
reject(new Error(‘fetch failed’));
}
}, 1000);
});
}
test(‘the data is grape (with .resolves)’, async () => {
await expect(fetchDataWithPromise(true)).resolves.toBe(‘grape’);
});
test(‘the fetch fails (with .rejects)’, async () => {
await expect(fetchDataWithPromise(false)).rejects.toThrow(‘fetch failed’);
});
“`
.resolves
や .rejects
の後に、通常のマッチャー (.toBe
, .toEqual
, .toThrow
など) をチェーンして使うことができます。この方法が非同期 Promise テストの最もクリーンな書き方とされています。
Code Coverage (コードカバレッジ):テストでどれだけコードがカバーされているか
コードカバレッジとは、テスト実行時にあなたのコードのどの部分(行、分岐、関数、ステートメントなど)が実行されたかを示す指標です。これにより、「書いたテストがコード全体に対してどれだけ網羅性を持っているか」を把握することができます。
Jestは、特に設定なしでコードカバレッジを測定し、レポートを出力する機能を内蔵しています。
実行方法 (--coverage
)
Jestのカバレッジレポートを取得するには、テスト実行時に --coverage
オプションをつけます。
bash
npm test -- --coverage
または
bash
yarn test --coverage
テスト実行後、以下のようなサマリーがターミナルに表示されます。
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 100 | 87.5 |
src/math.js | 100 | 100 | 100 | 100 |
src/api.js | 50 | 100 | 50 | 50 | 2,5
src/utils.js | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
また、プロジェクトのルートディレクトリに coverage
というディレクトリが生成され、その中に詳細なHTMLレポートが含まれます。 coverage/lcov-report/index.html
をブラウザで開くと、ファイルごとにコードのどの行がテストで実行されたか(緑)、実行されなかったか(赤)を視覚的に確認できます。
レポートの読み方
サマリーレポートには、以下の項目が表示されます。
% Stmts
: ステートメント(文)のカバレッジ率。単純な処理の単位が実行された割合。% Branch
: 分岐(if文やswitch文など)のカバレッジ率。条件分岐の各パス(真/偽)が実行された割合。% Funcs
: 関数のカバレッジ率。定義された関数が呼び出された割合。% Lines
: 行のカバレッジ率。コードの各行が実行された割合。Uncovered Line #s
: テストでカバーされなかった行番号。
これらの指標は、テストがコードをどれだけ網羅しているかの参考にすることができます。ただし、カバレッジ率が高いからといって、必ずしもテストの品質が高いとは限りません。単にコードを実行するだけでなく、様々な入力値やエッジケースに対して期待通りの振る舞いをするかを検証することが重要です。
カバレッジは、主に「テストが書かれていない危険な領域」を見つけるための補助ツールとして活用するのが良いでしょう。
カバレッジの設定
Jestの設定で、カバレッジに関する様々なオプションを指定できます。例えば、
collectCoverageFrom
: カバレッジ測定の対象とするファイルを指定します。特定のディレクトリやファイルパターンのみを対象にしたい場合に便利です。coverageThreshold
: カバレッジ率の最低値を設定します。これより低いとテスト実行が失敗するようにできます(CIなどで利用)。coverageReporters
: 出力するレポート形式を指定します(html, text, clover, jsonなど)。
これらの設定は、package.json
の jest
フィールド、または Jest の設定ファイルで行います。
json
// package.json
{
// ...
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}", // src ディレクトリ以下の .js, .jsx, .ts, .tsx ファイルを対象
"!src/**/*.d.ts" // ただし、.d.ts ファイルは除く
],
"coverageThreshold": {
"global": {
"branches": 80, // 全体で分岐80%以上
"functions": 80, // 全体で関数80%以上
"lines": 80, // 全体で行80%以上
"statements": 80 // 全体でステートメント80%以上
}
}
// ... 他のJest設定
}
}
カバレッジは、テスト戦略の一部として利用し、テストの網羅性を改善するための手がかりとして活用しましょう。
Configuration (設定):Jestのカスタマイズ
Jestは「設定不要で動く」のが魅力ですが、もちろん必要に応じて様々なカスタマイズが可能です。設定は主に以下のいずれかの方法で行います。
package.json
のjest
フィールド: シンプルな設定であれば、package.json
の中にjest
というキーを追加して、その中に設定オブジェクトを記述するのが最も簡単です。- Jest 設定ファイル: より複雑な設定や、設定を別ファイルに分けたい場合は、プロジェクトのルートディレクトリに設定ファイルを作成します。ファイル名は
jest.config.js
,jest.config.json
,jest.config.ts
,jest.config.mjs
,jest.config.cjs
などが利用可能です。.js
ファイルを使うと、JavaScriptで設定を記述できるため、より動的な設定やコメントを含めることができます。
package.json
の jest
フィールド
json
// package.json
{
// ...
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.0.0"
},
"jest": { // ★ ここにJestの設定を記述
"verbose": true, // より詳細なテスト結果を表示
"testEnvironment": "jsdom", // テスト環境をブラウザlikeにする (DOMなどを使うテストで必要)
"testMatch": [ // テストファイルを検索するパターン
"**/__tests__/**/*.js?(x)",
"**/?(*.)+(spec|test).js?(x)"
],
"setupFilesAfterEnv": ["./jest.setup.js"] // 各テストファイル実行後に読み込む設定ファイル
}
// ...
}
Jest 設定ファイル (jest.config.js
の例)
プロジェクトルートに jest.config.js
を作成し、以下のように記述します。
“`javascript
// jest.config.js
module.exports = {
// Jest のルートディレクトリを指定 (通常はプロジェクトルート)
rootDir: ‘./’,
// テスト対象の環境 (node または jsdom)
testEnvironment: ‘jsdom’,
// テストファイルを検索するパターン
testMatch: [
‘
‘
‘
],
// トランスパイルが必要なファイルの設定 (例: TypeScript, JSX)
// ‘babel-jest’ や ‘ts-jest’ などと組み合わせて使う
// transform: {
// ‘^.+\.jsx?$’: ‘babel-jest’,
// ‘^.+\.tsx?$’: ‘ts-jest’
// },
// モジュールエイリアスの設定 (Webpack や Parcel のエイリアスと連携させる場合など)
// 例: import Component from ‘@/components/Component’ の @ を解決
// moduleNameMapper: {
// ‘^@/(.*)$’: ‘
// },
// テストファイル実行前に一度だけ実行されるファイル
// setupFiles: [],
// 各テストファイル実行後に実行されるファイル (beforeEach, afterEach の設定など)
setupFilesAfterEnv: [‘
// モックしたいファイルのパターン (mocks ディレクトリ以外で)
// automock: false, // 通常は automock は false にする
// コードカバレッジの対象ファイル
collectCoverageFrom: [
‘src//*.{js,jsx,ts,tsx}’,
‘!src//*.d.ts’ // 型定義ファイルは除く
],
// カバレッジレポートの最低閾値
coverageThreshold: {
“global”: {
“branches”: 80,
“functions”: 80,
“lines”: 80,
“statements”: 80
}
},
// カバレッジレポートの出力形式
coverageReporters: [‘text’, ‘html’],
// テスト結果をより詳細に表示
verbose: true,
// テストモジュール検索パス (node_modules 以外も検索したい場合)
// modulePaths: [‘
// ファイル拡張子の優先順位
// moduleFileExtensions: [‘js’, ‘json’, ‘jsx’, ‘ts’, ‘tsx’, ‘node’],
};
“`
主要な設定オプション
testEnvironment
: テストを実行する環境を指定します。'node'
: Node.js 環境。サーバーサイドコードのテストに最適です。'jsdom'
: ブラウザのDOM APIをシミュレートする環境。ReactなどのUIコンポーネントテストで、DOM操作やイベント処理をテストする際に必要です。
testMatch
: Jestがテストファイルとして認識するファイルのパスのパターンを指定します。デフォルトは**/__tests__/**/*.js?(x)
や**/?(*.)+(spec|test).js?(x)
です。transform
: Jestは標準ではJavaScriptしか理解しませんが、BabelやTypeScriptコンパイラなどを使って、テスト実行時に他の言語や構文(TypeScript, JSXなど)をJavaScriptに変換するように設定できます。babel-jest
やts-jest
といったパッケージと組み合わせて使用します。moduleNameMapper
: モジュールのパスをマッピングするための設定です。例えば、Webpackなどでエイリアス(例:@/components
)を使っている場合に、Jestがそのエイリアスを解決できるようにするために必要です。setupFiles
/setupFilesAfterEnv
: 各テストファイルが実行される前や後に、特定のファイルを読み込むように設定します。例えば、テスト環境全体で利用する共通のセットアップ処理(ポリフィルの追加、グローバル変数の設定など)や、各テストファイルで共通のマッチャー拡張や初期化処理などを行う際に使います。collectCoverageFrom
/coverageThreshold
/coverageReporters
: コードカバレッジに関する設定です(前述)。verbose
:true
にすると、テスト結果の出力がより詳細になり、個々のテストケースの実行時間なども表示されます。
これらの設定オプションを活用することで、プロジェクトの構成や使用技術に合わせてJestを最適化し、より効率的で信頼性の高いテスト環境を構築できます。
実践的なJestの活用
Jestの基本的な機能を見てきましたが、実際の開発ではこれらの機能を組み合わせて使います。より良いテストコードを書くためのヒントや、Jestの応用的な活用方法にも触れておきましょう。
良いテストコードの書き方 (AAAパターンなど)
テストコードも、通常のアプリケーションコードと同様に、読みやすく、分かりやすく、保守しやすいように書くことが重要です。テストコードの構造を考える上でよく使われるのが、AAAパターンです。
- Arrange (準備): テストを実行するために必要な準備を行います。テスト対象のインスタンスの作成、モックの設定、入力データの準備などが含まれます。
- Act (実行): テスト対象のコード(関数呼び出しやメソッド実行など)を実際に実行します。
- Assert (検証): 実行結果が期待通りであるかをマッチャーを使って検証します。
このパターンに沿ってテストコードを書くと、テストの意図が明確になり、理解しやすくなります。
“`javascript
test(‘add function should return the sum of two numbers’, () => {
// Arrange (準備)
const num1 = 5;
const num2 = 10;
// Act (実行)
const result = add(num1, num2); // add 関数を呼び出す
// Assert (検証)
expect(result).toBe(15); // 結果が期待通りか検証
});
“`
特に複雑なテストケースを書く際に、AAAパターンを意識するとコードが整理されます。
テスト駆動開発 (TDD) とJest
テスト駆動開発(TDD)は、「失敗するテストを先に書き、そのテストを成功させるために必要最低限のコードを書き、最後にコードをリファクタリングする」というサイクルを繰り返す開発手法です。
JestはTDDと非常に相性が良いツールです。
- テストを書く: 実装する機能の仕様を満たすためのテストコードを、
describe
やtest
を使って書きます。この時点ではまだ実装がないので、テストは必ず失敗します。 - テストを実行し、失敗することを確認する:
npm test
を実行し、期待通りテストが失敗する(赤くなる)ことを確認します。 - コードを実装する: 書いたテストが成功するための必要最低限のコードを実装します。
- テストを実行し、成功することを確認する: 再び
npm test
を実行し、テストが成功する(緑になる)ことを確認します。 - リファクタリングする: 書いたコードを、テストが成功した状態を維持しつつ、より良い設計、より読みやすいコードになるようにリファクタリングします。リファクタリング後もテストを実行し、成功することを確認します。
- 繰り返す: 次の機能やケースに移り、このサイクルを繰り返します。
Jestの高速な実行、分かりやすいエラーメッセージ、test.only
やウォッチャーモード(後述)といった機能は、TDDサイクルをスムーズに回すのに役立ちます。TDDは単にテストを書くこと以上の効果(設計の改善、仕様理解の深化など)をもたらすため、ぜひJestと共に試してみてください。
ウォッチャーモード (--watch
)
Jestを --watch
オプション付きで実行すると、「ウォッチャーモード」に入ります。このモードでは、ファイルが変更されるたびに自動的に関連するテストを再実行してくれます。
bash
npm test -- --watch
または
bash
yarn test --watch
ウォッチャーモードは開発中に非常に便利です。コードを変更するたびに手動でテストを実行する必要がなくなり、すぐにテスト結果のフィードバックが得られます。デフォルトでは、変更されたファイルに関連するテストのみを実行しますが、Jestのウォッチャーには様々なオプションがあり、特定のテストを絞り込んだり、失敗したテストだけを再実行したりすることも可能です。
CI/CDパイプラインでのJest
継続的インテグレーション/継続的デリバリー (CI/CD) パイプラインにおいて、自動化されたテストの実行は不可欠です。JestはCI/CD環境での実行も容易です。
GitLab CI, GitHub Actions, CircleCI, JenkinsなどのCIサービスでは、ビルドステップの一部として npm test
(または yarn test
) コマンドを組み込むことができます。テストが失敗した場合、パイプラインは停止し、問題のある変更が本番環境にデプロールされるのを防ぎます。
また、CI環境では --coverage
オプションを付けてカバレッジを測定し、レポートを生成することも一般的です。設定ファイル (jest.config.js
など) で coverageThreshold
を設定しておけば、カバレッジが基準を満たさない場合にパイプラインを失敗させることも可能です。
Jestの出力は機械可読な形式(例: JUnit XML形式)で出力することもできるため、CIサービスのテストレポート機能と連携させることも容易です。
CI/CDパイプラインにJestのテストを組み込むことで、チーム開発における品質保証を自動化し、より安心してデプロイできるようになります。
まとめ:Jestと共に、自信を持ってコードを書こう!
この記事では、JavaScriptのテスティングフレームワークであるJestの基本的な機能を「超わかりやすく」解説しました。
- テストの重要性:なぜコードにテストが必要なのかを理解しました。
- Jestの導入:Jestをプロジェクトにインストールし、最初のテストを実行しました。
- 基本的な機能:
describe
/test
(it
):テストの構造化と個々のテストケース定義- Matchers:期待値と結果の多様な比較方法
- Setup / Teardown (
beforeEach
,afterAll
など):テスト前後の準備とクリーンアップ - Mocking (
jest.fn
,jest.spyOn
,jest.mock
):依存関係の排除と関数の監視 - Snapshot Testing (
toMatchSnapshot
):状態の変更検出 - Asynchronous Testing (
done
, Promise,async/await
,.resolves
,.rejects
):非同期処理のテスト - Code Coverage (
--coverage
):テスト網羅率の測定 - Configuration (
package.json
,jest.config.js
):Jestのカスタマイズ
これらの機能を使うことで、あなたのJavaScriptコードの信頼性を高め、開発効率を向上させることができます。バグを早期に発見し、安心してコードの変更やリファクタリングを行い、より堅牢なアプリケーションを構築するための強力な武器となります。
Jestは非常に多機能ですが、一度に全てを覚える必要はありません。まずは簡単なテストから始め、必要に応じてこの記事や公式ドキュメントを参照しながら、少しずつ機能を使いこなせるようになれば十分です。
次のステップ:
- あなたのプロジェクトにJestを導入してみましょう。
- 小さな機能やコンポーネントからテストを書き始めてみましょう。
- この記事で紹介した様々なマッチャーやモック機能を試してみましょう。
- 公式ドキュメントや他のチュートリアルも参考に、さらに理解を深めましょう。
テストを書くことは、最初は少し手間に感じるかもしれません。しかし、それは未来のあなたの時間と労力を節約するための投資です。Jestという頼れるパートナーと共に、自信を持って素晴らしいコードを書いていきましょう!