JavaScriptテスト Jest入門:特徴と使い方を解説

JavaScriptテスト Jest入門:特徴と使い方を解説

ソフトウェア開発において、品質保証と開発効率の向上は常に重要な課題です。特にJavaScriptのような動的な言語を用いた開発では、意図しないバグが混入しやすく、変更が既存の機能に影響を与える「デグレード」のリスクも高まります。これらの課題に対処するための強力な手段が「テスト」です。そして、JavaScript/TypeScriptのテストツールとして、近年最も人気があり、広く利用されているのがJestです。

この記事では、JavaScriptテストの重要性から説き起こし、なぜJestが多くの開発者に選ばれるのか、その特徴を詳しく解説します。さらに、Jestの基本的な導入方法から、アサーション、モック、非同期処理、スナップショットテストといった主要な機能の使い方を、具体的なコード例を交えながら詳細に紹介します。これからJavaScriptのテストを始めたい方、Jestに興味がある方、あるいはJestをより深く理解したい方にとって、この記事が確かな一歩を踏み出すための手助けとなれば幸いです。

1. なぜテストが必要なのか?テストの重要性

コードを書く際にテストを行うことは、単なる追加作業ではなく、開発プロセスにおいて不可欠な一部です。テストがなぜそれほど重要なのか、その理由を改めて考えてみましょう。

1.1. バグの早期発見と抑制

テストの最も直接的な目的は、コードに潜むバグを発見することです。特に、自動化されたテストを継続的に実行することで、バグを開発サイクルの早い段階で発見できます。開発初期に発見されたバグは、後工程(結合テスト、受け入れテスト、運用後)で発見されるバグに比べて、修正にかかるコスト(時間、労力、費用)が圧倒的に低いことが知られています。テストがない場合、バグはユーザーに届いてから初めて顕在化し、信頼性の低下や損害につながる可能性があります。

1.2. コードの品質向上

テストを書く過程で、開発者は自分のコードの振る舞いをより深く理解しようと努めます。特定の入力を与えたときに期待される出力は何か、どのようなエッジケースが存在するかなどを考えることで、より堅牢で正確なコードを書く意識が生まれます。また、テストしやすいコードは、一般的に関心の分離がなされており、単一責任の原則に従っている傾向があります。テスト容易性は、結果としてコードの可読性や保守性の向上にも繋がります。

1.3. リファクタリングの安全性確保

ソフトウェアは常に変化します。機能追加、バグ修正、パフォーマンス改善などのために、既存のコードを修正したり、構造を変えたり(リファクタリング)することは日常的に行われます。テストスイートが充実していれば、リファクタリングを行った後にテストを再実行することで、変更が既存の機能に悪影響を与えていないか(デグレードが発生していないか)を迅速に確認できます。テストは、開発者が自信を持ってコードを変更するためのセーフティネットとなります。

1.4. 仕様の明確化とドキュメンテーション

テストコードは、開発している機能の「動く仕様書」として機能します。特定の関数やコンポーネントがどのような入力に対してどのような出力を返すのか、どのような振る舞いをするのかが、テストコードを読むことで明確になります。これは、後からプロジェクトに参加した開発者や、機能の挙動を確認したい他の開発者にとって非常に役立ちます。

1.5. 開発速度の維持・向上

一見、テストを書くことで開発に時間がかかるように思えるかもしれません。しかし、テストがない場合に後から発生するバグ修正にかかる時間や、デグレードへの不安から慎重になりすぎることで失われる時間を考慮すると、自動テストの存在は長期的に見て開発速度を向上させます。特に、アジャイル開発のように短いサイクルで頻繁にリリースを行う開発プロセスにおいては、テストによる品質保証が必須となります。

2. Jestとは?JavaScriptテストのデファクトスタンダード

Jestは、Facebook(現Meta)によって開発されたJavaScriptのテストフレームワークです。Reactコンポーネントのテストのために開発が始まりましたが、現在ではReactに限らず、様々なJavaScript/TypeScriptプロジェクト(Vue, Angular, Node.jsなど)で広く利用されています。多くの開発者に選ばれている理由として、以下の特徴が挙げられます。

2.1. 導入と設定の容易さ(Zero-configuration)

Jestの最大の特徴の一つは、セットアップが非常に簡単なことです。多くの場合、プロジェクトにインストールしてpackage.jsonにテストスクリプトを追加するだけで、すぐにテストを書き始めることができます。BabelやTypeScriptなどのトランスパイラを使用している場合でも、Jestは自動的にそれらを検出して設定を推測してくれるため、複雑な設定ファイルを記述する手間が省けます。

2.2. 高速な実行

Jestは、テストの並列実行や、変更されたファイルに関連するテストだけをスマートに再実行する機能を備えており、テスト実行時間を短縮します。プロジェクトが大規模になり、テストケースが増えても、高速なフィードバックを得られるように設計されています。

2.3. オールインワンの統合ソリューション

他のテストフレームワークでは、テストランナー、アサーションライブラリ、モックライブラリ、カバレッジツールなどを個別に選択し、組み合わせて設定する必要がある場合があります。Jestはこれらの機能を全て内包しており、追加のライブラリを導入したり設定したりする手間がありません。

  • テストランナー: テストファイルを見つけ、実行する
  • アサーションライブラリ: expect など、テスト結果の期待値を記述する機能
  • モック機能: 外部依存(関数、モジュールなど)を置き換えてテストを分離する機能
  • カバレッジツール: テストがどの程度のコードをカバーしているかをレポートする機能
  • スナップショットテスト: UIやデータの構造的な変更を検出する機能

これらの機能が統合されていることで、一貫性のある開発体験が得られます。

2.4. スナップショットテストのサポート

Jestはスナップショットテスト機能を標準で提供しています。これは、Reactコンポーネントの出力や、大きな設定オブジェクトなどの「予期しない変更」を検出するのに非常に便利な機能です。最初のテスト実行時に出力(スナップショット)を保存し、以降の実行時に現在の出力と比較します。差分があればテストが失敗し、開発者はその差分が意図したものであるか、バグであるかを確認できます。

2.5. 優れたドキュメンテーションとコミュニティ

Jestの公式ドキュメントは非常に充実しており、機能ごとの詳細な解説、APIリファレンス、豊富な例が提供されています。また、広く利用されているため、Stack OverflowなどのQ&AサイトやGitHub上で多くの情報や解決策を見つけることができます。

これらの特徴から、JestはJavaScript/TypeScriptを用いた様々なプロジェクトにおけるテストフレームワークとして、第一の選択肢となっています。

3. テストの基本的な種類とJestの位置づけ

ソフトウェアテストには様々なレベルと種類があります。Jestは主に以下の種類のテストに使用されます。

3.1. ユニットテスト(Unit Test)

ユニットテストは、プログラムの最小単位(関数、メソッド、クラスなど)が意図した通りに動作するかを確認するテストです。他の部分から切り離し、単独で実行されます。Jestは、ユニットテストを書くための強力な機能(アサーション、モックなど)を提供しており、最も得意とする領域です。

3.2. 結合テスト(Integration Test)

結合テストは、複数のユニットが連携して動作する際に、それらが正しく統合されているかを確認するテストです。例えば、ある関数が別の関数を呼び出す場合や、コンポーネント同士が連携する場合などに適用されます。Jestは、ユニットテストだけでなく、ある程度の結合テストにも使用できます。特に、モック機能を使って外部システム(データベース、APIなど)への依存を排除することで、結合テストの範囲を制御できます。

3.3. E2Eテスト(End-to-End Test)

E2Eテストは、アプリケーション全体をユーザーの視点からテストするものです。UI操作(ボタンクリック、入力など)を通じて、システム全体が連携して要求されたビジネスプロセスを完了できるかを確認します。CypressやPlaywright、Seleniumなどの専用ツールがよく使われます。Jest単体でE2Eテストを行うことは一般的ではありませんが、Jestと組み合わせて使用されることはあります(例: E2Eテストの報告書作成にJestのレポーター機能を使うなど)。

Jestは主にユニットテストと、ある程度の結合テストに強みを持つテストフレームワークです。 アプリケーションのテスト戦略としては、これらのテスト種類を組み合わせることが一般的です。Jestでユニットテストをしっかり書き、主要な結合テストを行い、必要に応じてE2Eテストツールでユーザーシナリオをカバーするというアプローチが多く取られます。

4. Jestの導入と最初のテスト

それでは、実際にJestをプロジェクトに導入し、簡単なテストを書いて実行してみましょう。

4.1. 前提条件

  • Node.jsとnpmまたはYarnがインストールされていること。

4.2. プロジェクトの準備

新しいディレクトリを作成し、npmまたはYarnでプロジェクトを初期化します。

“`bash
mkdir jest-tutorial
cd jest-tutorial
npm init -y

または yarn init -y

“`

4.3. Jestのインストール

プロジェクトにJestを開発依存としてインストールします。

“`bash
npm install –save-dev jest

または yarn add –dev jest

“`

4.4. package.jsonの設定

package.jsonファイルを開き、scriptsセクションにテスト実行コマンドを追加します。

json
{
"name": "jest-tutorial",
"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がテストファイルを探して実行するようになります。

4.5. テスト対象のコードを作成

テスト対象となる簡単な関数を作成します。srcディレクトリを作成し、その中にmath.jsファイルを作成します。

“`javascript
// src/math.js

/*
* 二つの数値を加算します。
* @param {number} a – 最初の数値
* @param {number} b – 二番目の数値
* @returns {number} 合計
/
function add(a, b) {
return a + b;
}

/*
* 二つの数値を減算します。
* @param {number} a – 最初の数値
* @param {number} b – 二番目の数値
* @returns {number} 差
/
function subtract(a, b) {
return a – b;
}

module.exports = {
add,
subtract
};
“`

4.6. テストファイルを作成

Jestは、デフォルトで以下の命名規則に一致するファイルをテストファイルとして扱います。

  • .test.js または .spec.js で終わるファイル
  • __tests__ というディレクトリ内のファイル

今回は、srcディレクトリと同じ階層に__tests__ディレクトリを作成し、その中にmath.test.jsファイルを作成します。

bash
mkdir __tests__
touch __tests__/math.test.js

__tests__/math.test.jsファイルに、テストコードを記述します。

“`javascript
// tests/math.test.js

// テスト対象のモジュールをインポート
const { add, subtract } = require(‘../src/math’);

// add関数のテストグループを定義
describe(‘add function’, () => {
// 個々のテストケースを定義
test(‘should add two positive numbers correctly’, () => {
// 期待値と実際の値を比較 (アサーション)
expect(add(1, 2)).toBe(3);
});

test(‘should add a positive and a negative number correctly’, () => {
expect(add(5, -3)).toBe(2);
});

test(‘should add two negative numbers correctly’, () => {
expect(add(-1, -5)).toBe(-6);
});

test(‘should handle adding zero’, () => {
expect(add(10, 0)).toBe(10);
expect(add(0, 0)).toBe(0);
});
});

// subtract関数のテストグループを定義
describe(‘subtract function’, () => {
test(‘should subtract two positive numbers correctly’, () => {
expect(subtract(5, 3)).toBe(2);
});

test(‘should subtract a positive and a negative number correctly’, () => {
expect(subtract(10, -5)).toBe(15);
});

test(‘should handle subtracting zero’, () => {
expect(subtract(7, 0)).toBe(7);
expect(subtract(0, 0)).toBe(0);
});
});
“`

テストコードの解説

  • require('../src/math'): テスト対象の関数を含むモジュールを読み込んでいます。
  • describe('...'): 関連するテストケースをグループ化するために使用します。第一引数にグループ名、第二引数にそのグループ内のテストを定義する関数を指定します。ネストすることも可能です。
  • test('...', () => { ... }) または it('...', () => { ... }): 個々のテストケースを定義します。testitは完全に同じ機能です。第一引数にテストケースの名前(何を確認しているのかを明確に記述)、第二引数にテスト本体の処理を記述した関数を指定します。
  • expect(...): テスト対象のコードの実行結果(実際の値)を指定します。
  • .toBe(...): expectで指定した値が、引数で指定した期待値と厳密に等しい(===に相当)ことを検証するMatcher(マッチャー)です。Jestには様々なMatcherが用意されており、検証したい内容に応じて使い分けます。

4.7. テストの実行

コマンドラインでプロジェクトのルートディレクトリにいることを確認し、以下のコマンドを実行します。

“`bash
npm test

または yarn test

“`

Jestがテストファイルを探し、テストを実行して結果を表示します。テストが全て成功した場合、以下のような出力が得られます。

“`
PASS tests/math.test.js
add function
✓ should add two positive numbers correctly (2 ms)
✓ should add a positive and a negative number correctly (0 ms)
✓ should add two negative numbers correctly (0 ms)
✓ should handle adding zero (0 ms)
subtract function
✓ should subtract two positive numbers correctly (0 ms)
✓ should subtract a positive and a negative number correctly (0 ms)
✓ should handle subtracting zero (0 ms)

Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 0.XXX s
Ran all test suites.
“`

もしテストが失敗した場合、Jestはどのテストが失敗したか、期待値と実際の値は何かを詳細に表示してくれます。

これで、Jestを導入し、最初のテストを記述して実行することができました。

5. Jestのアサーション(Matchers)の詳細

テストコードにおいて、テスト対象のコードが期待通りの結果を返しているか、あるいは期待通りの状態になっているかを確認する部分をアサーション (Assertion) と呼びます。Jestではexpect関数とその後に続くメソッド群をMatcher(マッチャー)と呼び、様々なアサーションを記述できます。

ここでは、よく使われる主要なMatcherをいくつか紹介します。

5.1. 等価性の検証

  • toBe(value): プリミティブ型(文字列、数値、真偽値、null, undefined, シンボル)の厳密な等価性 (===) を検証します。

    “`javascript
    expect(1).toBe(1);
    expect(‘hello’).toBe(‘hello’);
    expect(true).toBe(true);
    expect(null).toBe(null);
    expect(undefined).toBe(undefined);

    // オブジェクトや配列の等価性にはtoBeは使いません!参照が同じかどうかを検証します。
    const obj1 = { a: 1 };
    const obj2 = { a: 1 };
    const obj3 = obj1;

    expect(obj1).not.toBe(obj2); // obj1とobj2は内容は同じだが、異なるオブジェクトなので失敗
    expect(obj1).toBe(obj3); // obj1とobj3は同じオブジェクトなので成功
    “`

  • toEqual(value): オブジェクトや配列の内容の等価性を再帰的に検証します。プロパティの値や配列の要素が同じであれば成功します。オブジェクトの比較には通常これを使用します。

    “`javascript
    const obj1 = { a: 1, b: { c: 2 } };
    const obj2 = { a: 1, b: { c: 2 } };
    expect(obj1).toEqual(obj2); // 内容が同じなので成功

    const arr1 = [1, { a: 2 }];
    const arr2 = [1, { a: 2 }];
    expect(arr1).toEqual(arr2); // 内容が同じなので成功
    “`

5.2. 真偽値、null, undefinedの検証

  • toBeNull(): 値がnullであることを検証します。
  • toBeUndefined(): 値がundefinedであることを検証します。
  • toBeDefined(): 値がundefinedではないことを検証します。
  • toBeTruthy(): 値が真偽値としてtrueと評価されることを検証します。(例: 非ゼロの数値, 空でない文字列, 配列, オブジェクトなど)
  • toBeFalsy(): 値が真偽値としてfalseと評価されることを検証します。(例: 0, ”, null, undefined, NaNなど)

    “`javascript
    let value;
    expect(value).toBeUndefined();

    value = null;
    expect(value).toBeNull();
    expect(value).toBeFalsy();

    value = 0;
    expect(value).toBeFalsy();

    value = ”;
    expect(value).toBeFalsy();

    value = 1;
    expect(value).toBeTruthy();

    value = ‘abc’;
    expect(value).toBeTruthy();

    value = {};
    expect(value).toBeTruthy();
    “`

5.3. 数値の比較

  • toBeGreaterThan(number): より大きい
  • toBeGreaterThanOrEqual(number): 以上
  • toBeLessThan(number): より小さい
  • toBeLessThanOrEqual(number): 以下
  • toBeCloseTo(number, precision?): 浮動小数点数の近似的な等価性を検証します。precisionは小数点以下の桁数を指定します(デフォルトは2)。

    “`javascript
    expect(10).toBeGreaterThan(5);
    expect(10).toBeLessThan(15);
    expect(10).toBeGreaterThanOrEqual(10);
    expect(10).toBeLessThanOrEqual(10);

    // 浮動小数点数の比較
    expect(0.1 + 0.2).not.toBe(0.3); // 浮動小数点演算の誤差のため失敗
    expect(0.1 + 0.2).toBeCloseTo(0.3); // 成功
    “`

5.4. 文字列の検証

  • toMatch(string | RegExp): 文字列が特定の文字列または正規表現にマッチすることを検証します。

    javascript
    expect('Hello World').toMatch('World');
    expect('Hello World').toMatch(/World/);
    expect('abc').not.toMatch(/d/);

5.5. 配列や反復可能なオブジェクトの検証

  • toContain(item): 配列または反復可能なオブジェクトに特定の要素が含まれていることを検証します。

    javascript
    const shoppingList = [
    'diapers',
    'kleenex',
    'trash bags',
    'paper towels',
    'beer'
    ];
    expect(shoppingList).toContain('beer');
    expect(new Set([1, 2, 3])).toContain(2);

5.6. 例外の検証

  • toThrow(error?): 特定の関数が例外を発生させることを検証します。オプションでエラーメッセージの文字列、正規表現、またはエラーインスタンスを指定して、発生したエラーが期待するものと一致するかを確認できます。

    “`javascript
    function compileAndroidCode() {
    throw new Error(‘you are using the wrong JDK’);
    }

    expect(() => compileAndroidCode()).toThrow(); // 例外が発生することだけを確認
    expect(() => compileAndroidCode()).toThrow(Error); // 特定のエラークラスであることを確認
    expect(() => compileAndroidCode()).toThrow(‘you are using the wrong JDK’); // 特定のエラーメッセージを確認
    expect(() => compileAndroidCode()).toThrow(/JDK/); // エラーメッセージが正規表現にマッチすることを確認
    “`

5.7. 関数呼び出しの検証(モック関数と組み合わせて使用)

モックのセクションで詳しく解説しますが、Jestのモック関数と組み合わせることで、関数が呼び出されたか、何回呼び出されたか、どのような引数で呼び出されたかなどを検証できます。

  • toHaveBeenCalled(): モック関数が一度でも呼び出されたことを検証します。
  • toHaveBeenCalledTimes(number): モック関数が特定の回数呼び出されたことを検証します。
  • toHaveBeenCalledWith(...args): モック関数が特定の引数のセットで呼び出されたことを検証します。

    “`javascript
    const mockCallback = jest.fn(); // モック関数を作成
    someFunction(mockCallback); // someFunction内でmockCallbackが呼び出されると想定

    expect(mockCallback).toHaveBeenCalled(); // mockCallbackが呼び出されたか?
    expect(mockCallback).toHaveBeenCalledTimes(1); // mockCallbackが1回呼び出されたか?
    expect(mockCallback).toHaveBeenCalledWith(‘first arg’, ‘second arg’); // 特定の引数で呼び出されたか?
    “`

5.8. 否定 (.not)

任意のMatcherの前に.notを付けることで、そのMatcherの条件を満たさないことを検証できます。

javascript
expect(1 + 1).not.toBe(3);
expect([1, 2]).not.toContain(3);
expect(() => {}).not.toThrow(); // 例外が発生しないことを検証

Jestには他にも多数のMatcherがあります。公式ドキュメントのMatcher APIを確認することをおすすめします。様々な状況に対応できるよう、豊富な検証手段が用意されています。

6. テストの構成:describe, test/it

前述の例でも使用しましたが、describetest (またはit) はJestでテストコードを構成する基本的な要素です。

  • describe(name, fn): 関連する複数のテストケースをグループ化するために使用します。nameはグループの名前を示す文字列、fnはそのグループ内のテストを定義する関数です。この関数内でさらにdescribeをネストしたり、testを定義したりできます。これにより、テストを階層的に整理し、可読性を高めることができます。
  • test(name, fn, timeout?) または it(name, fn, timeout?): 個々のテストケースを定義します。nameはテストケースの名前を示す文字列で、具体的に何を確認しているのかを記述します。fnは実際のテストロジックを含む関数です。オプションのtimeout引数で、そのテストケースのタイムアウト時間をミリ秒で指定できます。Jestでは、デフォルトのタイムアウトは5秒です。

例:

“`javascript
// 計算モジュールのテスト
describe(‘Calculator module’, () => {

// 足し算機能のテストグループ
describe(‘add function’, () => {
test(‘should add two positive numbers’, () => {
expect(add(2, 3)).toBe(5);
});

test('should add positive and negative number', () => {
  expect(add(5, -2)).toBe(3);
});

});

// 引き算機能のテストグループ
describe(‘subtract function’, () => {
test(‘should subtract two numbers’, () => {
expect(subtract(5, 3)).toBe(2);
});
});

// 掛け算機能のテストグループ (もしあれば)
// describe(‘multiply function’, …)
});
“`

このようにdescribeをネストすることで、例えば「フォームコンポーネントのテスト」の中に「入力フィールドのテスト」「送信ボタンのテスト」といったサブグループを作成し、テストコードの構造を明確にできます。良いテスト名とグループ名は、テストの目的と内容を素早く理解するために非常に重要です。

7. セットアップとティアダウン:beforeEach, afterEach, beforeAll, afterAll

複数のテストケースを実行する際に、それぞれのテストの前に共通の準備処理を行ったり、テストの後に共通の後処理を行ったりしたい場合があります。また、テストグループ全体で一度だけ実行したいセットアップやティアダウン処理もあるでしょう。Jestは、これらのニーズに応えるために便利なフック関数を提供しています。

7.1. 各テストケースの前後

  • beforeEach(fn, timeout?): describeブロック内で定義された各testまたはitが実行される直前に実行されます。
  • afterEach(fn, timeout?): describeブロック内で定義された各testまたはitが実行された直後に実行されます。

これらは、各テストが独立して実行されるように、テストごとに共通の初期状態を準備したり、使用したリソースをクリーンアップしたりするのに役立ちます。例えば、テストデータをリセットしたり、DOM要素を作成・削除したりする場合に使用します。

例:

“`javascript
let testData;

describe(‘Data processing tests’, () => {

// 各テストの前に実行される
beforeEach(() => {
console.log(‘Setting up test data…’);
testData = [1, 2, 3]; // 各テストケースごとに新しい配列を作成
});

// 各テストの後に実行される
afterEach(() => {
console.log(‘Cleaning up test data…’);
testData = null; // 使用したデータを解放
});

test(‘should process data correctly’, () => {
expect(testData.length).toBe(3);
// データ処理のテストロジック
});

test(‘should handle empty data’, () => {
testData = []; // このテスト用にデータを上書きすることも可能
expect(testData.length).toBe(0);
// 空データ処理のテストロジック
});
});
“`

7.2. 各テストグループ(describeブロック)の前後

  • beforeAll(fn, timeout?): describeブロック内の全てのテストケースが実行される前に一度だけ実行されます。
  • afterAll(fn, timeout?): describeブロック内の全てのテストケースが実行された後に一度だけ実行されます。

これらは、テストグループ全体で共有するリソース(例: データベース接続、テストサーバーの起動)のセットアップや、リソースの解放に使用します。各テストケースごとに実行する必要がない、コストの高い処理に適しています。

例:

“`javascript
let dbConnection;

describe(‘Database integration tests’, () => {

// テストグループ開始前に一度だけ実行される
beforeAll(async () => {
console.log(‘Establishing database connection…’);
// ダミーのDB接続処理を想定
dbConnection = await establishDBConnection();
// 必要ならテストデータを投入
await populateTestData(dbConnection);
});

// テストグループ終了後に一度だけ実行される
afterAll(async () => {
console.log(‘Closing database connection…’);
// DB接続を閉じる
await closeDBConnection(dbConnection);
});

// 各テストの前に実行される (必要に応じて)
beforeEach(() => {
// 各テスト用にデータをリセットするなど
});

test(‘should retrieve user data’, async () => {
const user = await dbConnection.getUserById(1);
expect(user).toEqual({ id: 1, name: ‘Test User’ });
});

test(‘should create a new user’, async () => {
const newUser = { name: ‘New User’ };
const createdUser = await dbConnection.createUser(newUser);
expect(createdUser).toHaveProperty(‘id’);
expect(createdUser.name).toBe(‘New User’);
});
});
“`

注意点:

  • beforeEachafterEachは、describeブロックのスコープ内で定義する必要があります。ネストされたdescribeブロック内のフックは、その内部のテストケースに対してのみ適用されます。
  • フック関数内で非同期処理を行う場合は、async/awaitを使用したり、Promiseを返したりすることで、Jestがその処理の完了を待ってから次のテストやフックに進むようにする必要があります。

これらのフックを適切に使うことで、テストの独立性を保ちつつ、効率的にテスト環境を準備・片付けできます。

8. モック(Mocking)の詳細

ユニットテストの目的は、テスト対象のコードの「ユニット」だけをテストすることです。しかし、実際のコードは他の関数、モジュール、外部システム(API、データベース、ファイルシステムなど)に依存していることがよくあります。これらの依存関係があると、ユニット単体を分離してテストすることが難しくなります。例えば:

  • 外部APIを呼び出す関数をテストしたいが、毎回APIを呼び出すのは遅いし、テスト環境によっては利用できない。
  • データベースからデータを取得する関数をテストしたいが、実際にDBに接続してデータを準備するのは面倒で、テスト実行ごとに状態が変わってしまう。
  • 特定の条件下でしか発生しないエラーをテストしたいが、その条件を作り出すのが難しい。

このような場合に役立つのがモック(Mock)機能です。モックとは、テスト対象のユニットが依存している他の要素(関数、モジュール、オブジェクトなど)を、テスト用に制御可能な「偽物」に置き換えることです。これにより、依存関係を断ち切り、テスト対象のユニットの振る舞いのみに焦点を当てたテストが可能になります。

Jestは強力で使いやすいモック機能を提供しています。主に以下の3つの方法があります。

  1. モック関数 (jest.fn()): 関数単体をモック化し、呼び出し状況を追跡したり、戻り値や実装を制御したりします。
  2. モックモジュール (jest.mock()): モジュール全体またはモジュール内の特定の関数を置き換えます。
  3. スパイ (jest.spyOn()): 既存のオブジェクトのメソッド呼び出しを監視しつつ、デフォルトでは元の実装をそのまま実行します。

8.1. モック関数 (jest.fn())

jest.fn()を使うと、関数呼び出しの記録(何回、どのような引数で呼び出されたか)を追跡できるモック関数を作成できます。また、そのモック関数が特定の戻り値を返すようにしたり、特定の実装を持つようにしたりすることも可能です。

使い方:

“`javascript
const mockCallback = jest.fn();

// このモック関数をどこかに渡して呼び出す
// 例えば、配列のforEachに渡す
[‘a’, ‘b’, ‘c’].forEach(mockCallback);

// モック関数の呼び出し状況を検証
expect(mockCallback).toHaveBeenCalled(); // 少なくとも1回呼び出されたか
expect(mockCallback).toHaveBeenCalledTimes(3); // 3回呼び出されたか
expect(mockCallback).toHaveBeenCalledWith(‘a’, 0, [‘a’, ‘b’, ‘c’]); // 特定の引数で呼び出されたか
expect(mockCallback).toHaveBeenLastCalledWith(‘c’, 2, [‘a’, ‘b’, ‘c’]); // 最後に呼び出された引数は何か
“`

戻り値や実装の制御:

  • mockReturnValue(value): モック関数が常に指定したvalueを返します。

    javascript
    const mockFn = jest.fn().mockReturnValue('mock value');
    expect(mockFn()).toBe('mock value'); // 常に 'mock value' を返す
    expect(mockFn()).toBe('mock value');

  • mockReturnValueOnce(value): モック関数が一度だけ指定したvalueを返します。複数回チェーンできます。一度返された後は、もし他のmockReturnValueOnceがなければundefinedを返します(あるいはmockReturnValueが設定されていればそちらを返します)。

    “`javascript
    const mockFn = jest.fn();
    mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValue(3);

    expect(mockFn()).toBe(1); // 1回目は 1
    expect(mockFn()).toBe(2); // 2回目は 2
    expect(mockFn()).toBe(3); // 3回目以降は 3
    expect(mockFn()).toBe(3);
    “`

  • mockImplementation(fn): モック関数が呼び出されたときに実行される独自の関数実装を指定します。複雑な振る舞いをシミュレートしたい場合に便利です。

    javascript
    const mockFn = jest.fn().mockImplementation((a, b) => a + b);
    expect(mockFn(1, 2)).toBe(3); // 独自の加算ロジックを実行
    expect(mockFn(5, 5)).toBe(10);

  • mockImplementationOnce(fn): モック関数が一度だけ実行される独自の関数実装を指定します。

  • mockResolvedValue(value): 非同期モック関数が解決する値を指定します(Promise.resolve(value) 相当)。

    javascript
    const mockAsyncFn = jest.fn().mockResolvedValue('async result');
    await expect(mockAsyncFn()).resolves.toBe('async result');

  • mockRejectedValue(error): 非同期モック関数が拒否する値を指定します(Promise.reject(error) 相当)。

    javascript
    const mockAsyncFn = jest.fn().mockRejectedValue(new Error('async error'));
    await expect(mockAsyncFn()).rejects.toThrow('async error');

jest.fn()は、コールバック関数や、テスト対象の関数に依存として渡される関数をモックする際に非常に役立ちます。

8.2. モックモジュール (jest.mock())

jest.mock(moduleName, factory?)を使うと、指定したモジュール全体またはモジュール内の特定のexportsをモック化できます。これは、テスト対象のファイルが他のモジュールをrequireimportで依存している場合に便利です。

例えば、api.jsというモジュールがあり、その中のfetchUser関数が外部APIを呼び出すとします。

“`javascript
// src/api.js
const axios = require(‘axios’);

async function fetchUser(userId) {
const response = await axios.get(/api/users/${userId});
return response.data;
}

module.exports = { fetchUser };
“`

このfetchUser関数に依存しているuserService.jsというファイルをテストしたいとします。

“`javascript
// src/userService.js
const api = require(‘./api’);

async function getUserDetails(userId) {
const user = await api.fetchUser(userId);
if (!user || !user.isActive) {
return null;
}
return ${user.name} (${user.email});
}

module.exports = { getUserDetails };
“`

getUserDetailsをテストする際に、実際のapi.fetchUserを呼び出したくない(APIコールしたくない)ので、apiモジュールをモックします。

“`javascript
// tests/userService.test.js

// ‘./api’ モジュール全体をモック化
jest.mock(‘../src/api’);

// モック化されたapiモジュールをインポート
const api = require(‘../src/api’);
const { getUserDetails } = require(‘../src/userService’);

describe(‘getUserDetails’, () => {

// 各テストの前に、モック関数をリセットする(重要!)
// これをしないと、前のテストでのモックの呼び出し状況などが引き継がれてしまう
beforeEach(() => {
// jest.clearAllMocks(); // 全てのモックの呼び出し状況などをクリア
jest.resetAllMocks(); // 全てのモックの呼び出し状況などをクリアし、mockImplementationなどもリセット
// jest.restoreAllMocks(); // jest.spyOnで作成したモックを元の実装に戻す
});

test(‘should return user details if user is active’, async () => {
// モック化されたapi.fetchUserの実装を設定
// resolveする値を指定
api.fetchUser.mockResolvedValue({
id: 1,
name: ‘Test User’,
email: ‘[email protected]’,
isActive: true,
});

const details = await getUserDetails(1);
expect(details).toBe('Test User ([email protected])');
// getUserDetailsがapi.fetchUserを呼び出したか確認
expect(api.fetchUser).toHaveBeenCalledWith(1);

});

test(‘should return null if user is not active’, async () => {
api.fetchUser.mockResolvedValue({
id: 2,
name: ‘Inactive User’,
email: ‘[email protected]’,
isActive: false, // 非アクティブ
});

const details = await getUserDetails(2);
expect(details).toBeNull();
expect(api.fetchUser).toHaveBeenCalledWith(2);

});

test(‘should return null if user is not found’, async () => {
// ユーザーが見つからない場合、fetchUserがnullやundefinedを返すと想定
api.fetchUser.mockResolvedValue(null);

const details = await getUserDetails(3);
expect(details).toBeNull();
expect(api.fetchUser).toHaveBeenCalledWith(3);

});
});
“`

モックモジュールのファクトリー関数:

jest.mock()の第二引数にファクトリー関数(モックモジュールの実装を返す関数)を指定することもできます。これにより、モックモジュールが返す値をより細かく制御したり、モックの実装を遅延させたりできます。

“`javascript
jest.mock(‘../src/api’, () => ({
// fetchUser関数をモック関数に置き換える
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: ‘Mock User’, email: ‘[email protected]’, isActive: true })),
// もしapiモジュールが他のexportを持っていれば、それもここに定義する必要がある
otherFunction: jest.fn(),
}));

// … テストコード …

// この場合、api.fetchUserはjest.fn()であり、上記のmockResolvedValueと同じように振る舞う
expect(api.fetchUser).toHaveBeenCalledWith(1);
“`

ファクトリー関数を使用する場合、モジュール内で定義された他のエクスポートも明示的にモック化しないと、それらがundefinedになることに注意が必要です。元のモジュールの一部だけをモックしたい場合は、後述するjest.spyOn()が便利な場合があります。

8.3. スパイ (jest.spyOn())

jest.spyOn(object, methodName)は、既存のオブジェクトのメソッド呼び出しを「監視(spy)」するための機能です。jest.spyOnで作成されたモック関数は、デフォルトでは元のメソッドの実装を実行しますが、mockReturnValuemockImplementationなどを使用して、元の実装を一時的に置き換えたり、戻り値を変更したりすることも可能です。

これは、元のメソッドを実行したいが、そのメソッドが呼び出されたか、どのような引数で呼び出されたかなどを確認したい場合に便利です。

使い方:

``javascript
// src/utils.js
const utils = {
fetchData: (id) => {
console.log(
Fetching data for ID: ${id});
// 実際のデータ取得処理(例えば非同期)
return Promise.resolve({ id, value: 'some data' });
},
processData: (data) => {
console.log('Processing data:', data);
return
Processed: ${data.value}`;
},
};

module.exports = utils;
“`

processData関数がfetchData関数を内部で呼び出しているとして、processDataのテストを書きたいが、fetchDataの実行は監視したいが、テストによってはfetchDataの結果を制御したい場合を考えます。

“`javascript
// src/service.js
const utils = require(‘./utils’);

async function getDataAndProcess(id) {
const data = await utils.fetchData(id);
if (!data) {
return null;
}
return utils.processData(data);
}

module.exports = { getDataAndProcess };
“`

getDataAndProcessをテストします。

“`javascript
// tests/service.test.js

const utils = require(‘../src/utils’);
const { getDataAndProcess } = require(‘../src/service’);

describe(‘getDataAndProcess’, () => {
// spyOnで作成したモックを元に戻すためのクリーンアップ
afterEach(() => {
jest.restoreAllMocks();
});

test(‘should fetch data and process it’, async () => {
// utils.fetchDataメソッドへの呼び出しを監視するスパイを作成
// デフォルトでは元の実装が実行される
const fetchDataSpy = jest.spyOn(utils, ‘fetchData’);
const processDataSpy = jest.spyOn(utils, ‘processData’); // processDataも監視

const result = await getDataAndProcess(1);

// fetchDataが呼び出されたか、引数は何かを確認
expect(fetchDataSpy).toHaveBeenCalledWith(1);
// processDataが呼び出されたか、fetchDataの戻り値で呼び出されたかを確認
expect(processDataSpy).toHaveBeenCalledWith({ id: 1, value: 'some data' });
// 結果を確認
expect(result).toBe('Processed: some data');

// 元の実装が呼び出されたことを確認したい場合にspyOnは有効
// (この例では、fetchDataが実際にPromise.resolveを返したことを確認)

});

test(‘should return null if fetchData returns null’, async () => {
// utils.fetchDataがnullを返すように一時的に実装を置き換える
const fetchDataSpy = jest.spyOn(utils, ‘fetchData’).mockResolvedValue(null);
// processDataへの呼び出しは監視するが、このケースでは呼び出されないはず
const processDataSpy = jest.spyOn(utils, ‘processData’);

const result = await getDataAndProcess(2);

expect(fetchDataSpy).toHaveBeenCalledWith(2);
expect(result).toBeNull();
// processDataは呼び出されないはず
expect(processDataSpy).not.toHaveBeenCalled();

// jest.restoreAllMocks() が afterEach で実行されることで、
// 他のテストでは utils.fetchData は元の実装に戻る

});
});
“`

jest.spyOnは、既存のオブジェクトのメソッドに対して使う場合に特に便利です。jest.mockのようにモジュール全体を置き換えるのではなく、特定のメソッドだけを対象としたい場合に適しています。また、jest.restoreAllMocks()を使うことで、spyOnで作成したモック(スパイ)をテスト後に元の実装に戻すことができる点も特徴です。

モック機能のまとめ:

  • jest.fn(): 関数単体をモック化・監視したい場合に。
  • jest.mock(): モジュール全体やその中の特定のエクスポートを完全に置き換えたい場合に。特に外部ライブラリやフレームワークの依存を排除する際に有効。
  • jest.spyOn(): 既存のオブジェクトのメソッド呼び出しを監視しつつ、必要に応じて一時的に振る舞いを変更したい場合に。元の実装を活かしつつ監視したい場合に便利。

これらのモック機能を使いこなすことで、依存関係の複雑なコードでも、単体のユニットテストを効果的に記述できるようになります。

9. 非同期処理のテスト

JavaScriptでは非同期処理(Promise, async/await, callback, timerなど)が頻繁に使用されます。Jestは非同期処理のテストも容易に行えるようサポートしています。

9.1. Promiseのテスト

テスト対象の関数がPromiseを返す場合、以下の方法でテストできます。

  • .then()done(): Promiseが解決された後にdone()コールバックを呼び出すことで、テストの終了をJestに伝えます。

    javascript
    test('the data is peanut butter', () => {
    expect.assertions(1); // 非同期テストではアサーションが実行されたかを確認するために重要
    return fetchData().then(data => {
    expect(data).toBe('peanut butter');
    // done(); // Promiseを返す場合はdone()は不要
    });
    });

    ※ Promiseを返すテスト関数の場合、JestはPromiseが解決または拒否されるのを自動的に待つため、done()は不要です。ただし、後述のCallbackの場合には必要になります。

  • .resolves.rejects: expectに続けて.resolvesまたは.rejectsを使用し、Promiseが解決/拒否される値を直接Matcherで検証できます。

    “`javascript
    test(‘the data is peanut butter’, () => {
    // fetchData() が ‘peanut butter’ で解決することを検証
    return expect(fetchData()).resolves.toBe(‘peanut butter’);
    });

    test(‘the fetch fails with an error’, () => {
    // fetchData() が特定のエラーで拒否されることを検証
    return expect(fetchDataThatFails()).rejects.toThrow(‘error’);
    });
    “`

  • async/await: テスト関数をasyncで定義し、awaitでPromiseの解決を待つのが最も一般的で推奨される方法です。

    “`javascript
    test(‘the data is peanut butter’, async () => {
    const data = await fetchData();
    expect(data).toBe(‘peanut butter’);
    });

    test(‘the fetch fails with an error’, async () => {
    expect.assertions(1); // try/catchを使う場合はexpect.assertions()が重要
    try {
    await fetchDataThatFails();
    } catch (e) {
    expect(e.message).toBe(‘error’);
    }
    });

    // async/awaitと.resolves/.rejectsを組み合わせることも可能
    test(‘the data is peanut butter (async/resolves)’, async () => {
    await expect(fetchData()).resolves.toBe(‘peanut butter’);
    });
    ``async/awaitは非同期コードを同期的に書いているかのように扱えるため、テストコードの可読性が向上します。非同期テストでは、少なくとも1つのアサーションが実行されることを保証するためにexpect.assertions(numberOfAssertions)を使用することが推奨されます。特にtry/catchで例外を捕捉するテストケースでは、expectが一度も実行されない場合にテストが成功と誤判定されるのを防ぐため、expect.assertions()`は必須です。

9.2. Callbackのテスト

Node.jsの伝統的なコールバック形式の非同期処理をテストする場合、テスト関数にdone引数を渡します。非同期処理が完了し、アサーションが実行された後に、このdone()コールバックを呼び出すことで、Jestにテストが終了したことを伝えます。done()が呼び出されない場合、テストはタイムアウトします。

“`javascript
// fetchDataWithCallback は (callback) => { … callback(error, data); } の形式と想定

test(‘the data is peanut butter’, (done) => {
function callback(error, data) {
try {
expect(error).toBeNull(); // エラーがないことを確認
expect(data).toBe(‘peanut butter’);
done(); // 非同期処理とアサーションが完了したらdone()を呼び出す
} catch (error) {
// アサーションが失敗した場合もdone(error)を呼び出してJestに失敗を伝える
done(error);
}
}

fetchDataWithCallback(callback);
});
``done()を呼び忘れるとテストがタイムアウトで失敗します。また、アサーションがtry/catchブロックの外にある場合、アサーション失敗はJestによって捕捉されますが、try/catch内でアサーションを行う場合は、エラーをdone()`に渡す必要があります。

9.3. Timerのテスト (jest.useFakeTimers())

setTimeout, setInterval, clearTimeout, clearIntervalなどのタイマー関連の関数に依存するコードをテストする場合、実際の時間経過を待つのは非効率です。Jestは偽のタイマー機能を提供しており、時間を「早送り」することでタイマー関連のテストを高速化できます。

使い方:

“`javascript
// テストの前に偽のタイマーを有効化
beforeEach(() => {
jest.useFakeTimers();
});

// テストの後にタイマーを元に戻す(任意だが推奨)
afterEach(() => {
jest.useRealTimers();
});

test(‘should execute the callback after 1 second’, () => {
const callback = jest.fn();

// 1秒後にcallbackが実行される関数をテスト対象とする
function delayedCall(fn) {
setTimeout(fn, 1000);
}

delayedCall(callback);

// この時点ではコールバックは実行されていないはず
expect(callback).not.toHaveBeenCalled();

// 1秒分時間を進める
jest.advanceTimersByTime(1000);

// 1秒経過したのでコールバックが実行されたはず
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

test(‘should execute the interval callback multiple times’, () => {
const callback = jest.fn();

function intervalCall(fn) {
setInterval(fn, 500);
}

intervalCall(callback);

expect(callback).not.toHaveBeenCalled();

// 500ms進める -> 1回呼び出される
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);

// さらに500ms進める (合計1000ms) -> さらに1回呼び出される
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(2);

// さらに200ms進める (合計1200ms) -> まだ呼び出されない
jest.advanceTimersByTime(200);
expect(callback).toHaveBeenCalledTimes(2);

// さらに300ms進める (合計1500ms) -> さらに1回呼び出される
jest.advanceTimersByTime(300);
expect(callback).toHaveBeenCalledTimes(3);
});

// プロミスタイマー (例: process.nextTick, setImmediate) を進める場合は
// jest.advanceTimersToNextTimerAsync() (async/awaitと併用) を使う
test(‘should handle process.nextTick’, async () => {
const callback = jest.fn();
process.nextTick(callback);

// この時点ではまだ実行されていない
expect(callback).not.toHaveBeenCalled();

// PromiseキューやTimerキューを進めて、次のタイマー/マイクロタスクを実行
await jest.advanceTimersToNextTimerAsync();

// nextTickが実行されたはず
expect(callback).toHaveBeenCalledTimes(1);
});
``jest.useFakeTimers()を呼び出すと、組み込みのタイマー関数がJestの偽のタイマーに置き換えられます。jest.advanceTimersByTime(ms)で指定したミリ秒だけ時間を早送りし、その間に実行されるはずだったタイマーコールバックを実行します。jest.runAllTimers()を使うと、スケジュールされている全てのタイマーコールバックを即座に実行できます。jest.advanceTimersToNextTimerAsync()`は、Promiseキューを含む次のマイクロタスク/タイマーを実行するために使用され、非同期操作を伴うタイマー処理のテストに役立ちます。

これらの非同期テスト手法を理解し、適切に使い分けることで、JavaScriptの非同期コードも自信を持ってテストできるようになります。

10. スナップショットテスト

スナップショットテストは、UIコンポーネントのレンダリング結果や、大きな設定オブジェクト、データ構造などが「予期せず変更されていないか」を検出するための便利な機能です。

仕組み:

  1. 最初のテスト実行時に、テスト対象の出力(例: ReactコンポーネントのHTML構造、JSONオブジェクト)をシリアライズし、.snapという拡張子を持つファイルに保存します。これが「スナップショット」です。
  2. 以降のテスト実行時には、テスト対象の現在の出力を計算し、保存されているスナップショットと比較します。
  3. 現在の出力とスナップショットに差分があれば、テストは失敗します。開発者はその差分を確認し、それが意図した変更であればスナップショットを更新し、意図しないバグであればコードを修正します。

10.1. 使い方

スナップショットテストは、expect.toMatchSnapshot()または.toMatchInlineSnapshot()というMatcherを使用して行います。

  • toMatchSnapshot(): スナップショットを.snapファイルとして別途保存します。

    “`javascript
    // 例: Reactコンポーネントのテスト (Testing Library と組み合わせて使用)
    import { render } from ‘@testing-library/react’;
    import MyComponent from ‘../src/MyComponent’;

    test(‘MyComponent should render correctly’, () => {
    const { container } = render();
    // レンダリング結果のHTML構造のスナップショットを作成/比較
    expect(container).toMatchSnapshot();
    });

    // 例: JSONオブジェクトのスナップショットテスト
    test(‘should return the correct configuration object’, () => {
    const config = generateConfig(); // 設定オブジェクトを生成する関数
    // 生成されたオブジェクトのスナップショットを作成/比較
    expect(config).toMatchSnapshot();
    });
    ``
    初回実行時、
    tests/snapshots/MyComponent.test.js.snap` のようなファイルが生成され、テスト対象のシリアライズされた出力が保存されます。

  • toMatchInlineSnapshot(): スナップショットをテストコードファイル内のMatcherの引数としてインラインで保存します。.snapファイルが別途作成されないため、簡単なスナップショットの場合にコードとスナップショットを一緒に管理できて便利です。

    javascript
    test('should return a simple object', () => {
    const obj = { id: 1, name: 'test' };
    // スナップショットがテストファイル内に保存される
    expect(obj).toMatchInlineSnapshot(`
    Object {
    "id": 1,
    "name": "test",
    }
    `);
    });

    初回実行時、toMatchInlineSnapshot()の引数部分は空の文字列()になります。テスト実行後、Jestが自動的にそこにシリアライズされた出力を書き込みます。

10.2. スナップショットの更新

コードを変更して意図的にスナップショットの内容が変わる場合、テストは失敗します。このとき、Jestは差分を表示し、スナップショットを更新するためのコマンドを教えてくれます。通常は以下のコマンドを実行します。

“`bash
npm test — -u

または yarn test -u

``
このコマンドを実行すると、失敗したスナップショットテストについて、現在の出力でスナップショットファイル(
.snap`ファイルまたはインラインスナップショット)が上書き更新されます。更新後に再度テストを実行すると、テストが成功するようになります。

10.3. スナップショットテストの注意点

  • 頻繁に変わる値を含めない: 日付、時刻、ランダムなIDなど、実行ごとに値が変わるものを含めると、スナップショットテストは常に失敗するようになり、役に立たなくなります。これらの値は、テスト時に固定値に置き換えたり、Matcherを使って範囲や形式を検証したりする必要があります。
  • スナップショットのレビュー: スナップショットファイルもコードリポジトリにコミットすべき開発資産です。プルリクエストなどでコードレビューを行う際は、変更されたスナップショットファイルも必ずレビューし、その差分が意図したものであることを確認することが重要です。意図しない差分はバグの兆候かもしれません。
  • 過信しない: スナップショットテストは「意図しない変更」を検出するのに優れていますが、「正しい振る舞い」そのものを検証するものではありません。特定の入力に対する正確な出力など、ビジネスロジックのテストは、引き続き具体的なMatcherを使った単体テストで行うべきです。スナップショットテストは、従来のMatcherによるテストを補完するものとして位置づけるのが良いでしょう。
  • 大きなスナップショット: スナップショットが非常に大きくなると、.snapファイルの可読性が低下し、レビューが難しくなります。必要に応じて、テスト対象を分割したり、スナップショットに含める情報を限定したりすることを検討してください。

スナップショットテストは、特にUIコンポーネントの視覚的な変化や、複雑な出力構造の変更を効率的に検出する強力なツールです。適切に利用することで、開発効率と品質維持に貢献します。

11. テストカバレッジレポート

テストカバレッジ(Test Coverage)とは、テストコードが実際のプロダクションコードのどの程度をカバーしているかを示す指標です。カバレッジレポートは、テストが全く書かれていない、あるいは十分でないコード領域を特定するのに役立ちます。Jestはテストカバレッジレポートの生成機能を標準で備えています。

11.1. カバレッジレポートの生成

Jestに--coverageフラグを付けてテストを実行すると、テスト実行後にカバレッジレポートが生成されます。

“`bash
npm test — –coverage

または yarn test –coverage

``
または、
package.jsontestスクリプトに–coverageを追加しておくと、npm test`実行時に常にカバレッジが生成されるようになります。

json
"scripts": {
"test": "jest --coverage"
}

実行後、Jestはコンソールにサマリーを表示し、プロジェクトルートにcoverageディレクトリを作成して詳細なレポートファイル(HTML形式など)を出力します。

11.2. レポートの見方

カバレッジレポートには通常、以下の4つの指標が含まれます。

  • Stmts (Statements): ステートメント(文)のカバレッジ。実行されたステートメントの割合。
  • Branch: ブランチ(分岐)のカバレッジ。if/else? :&&/||などの条件分岐において、考えられる全ての経路のうち実行された経路の割合。
  • Funcs (Functions): 関数のカバレッジ。定義された関数のうち、呼び出された関数の割合。
  • Lines: 行のカバレッジ。実行されたコード行の割合。

例えば、コンソールのサマリーは以下のような形式で表示されます。

--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 90.52 | 75.34 | 93.12 | 90.48 |
src/math.js | 100 | 100 | 100 | 100 |
src/utils.js | 85.71 | 50 | 100 | 85.71 | 7,8
--------------|---------|----------|---------|---------|-------------------

この例では、src/math.jsは完全にカバーされていますが、src/utils.jsはステートメント、ブランチ、行のカバレッジが100%ではありません。特にブランチカバレッジが50%なのは、条件分岐の一方しかテストできていないことを示唆しています。Uncovered Line #sは、カバーされていない行番号を示しており、テストを追加すべき箇所を特定するのに役立ちます。

coverageディレクトリ内のlcov-report/index.htmlファイルを開くと、より詳細なHTML形式のレポートを確認できます。各ファイルごとに、カバーされている行とカバーされていない行が色分けして表示され、どの分岐が実行されていないかなども確認できます。

11.3. カバレッジの目標設定

カバレッジは100%を目指すべきでしょうか?理想的にはそうですが、現実的には難しい場合や、100%に固執することが非効率な場合もあります。例えば、エラーが発生する可能性が極めて低いコード経路や、シンプルなgetter/setterなど、テストの費用対効果が低い箇所もあります。

重要なのは、カバレッジの「数値」だけを目標にするのではなく、カバレッジレポートを「テストが不十分な領域を見つけるためのツール」として活用することです。特に、アプリケーションの主要なロジック、複雑な分岐、エラーハンドリング、外部システムとの連携部分などは、優先的に高いカバレッジを確保すべきです。

チームやプロジェクトの特性に応じて、現実的なカバレッジの目標値(例: 80%以上)を設定し、CI/CDパイプラインに組み込むことで、コードの品質を一定レベルに保つことができます。Jestの設定オプション(後述)で、カバレッジの閾値を設定し、それを満たさない場合にテストを失敗させることも可能です。

12. Jestの設定オプション

Jestは多くの設定オプションを持っており、プロジェクトの要件に合わせてカスタマイズできます。設定はpackage.jsonjestフィールドに記述するか、jest.config.js(またはjest.config.ts, jest.config.jsonなど)という設定ファイルを作成して記述するのが一般的です。設定ファイルを使う方が、設定項目が多い場合に整理しやすいため推奨されます。

12.1. package.jsonでの設定

json
{
"name": "my-project",
"jest": {
"verbose": true,
"coverageThreshold": {
"global": {
"branches": 75,
"functions": 80,
"lines": 80,
"statements": 80
}
},
"moduleFileExtensions": [
"js",
"json",
"jsx",
"ts",
"tsx",
"node"
],
"testMatch": [
"**/__tests__/**/*.js?(x)",
"**/?(*.)+(spec|test).js?(x)"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
]
}
}

12.2. jest.config.jsでの設定

jest.config.jsファイルをプロジェクトルートに作成します。JavaScriptファイルなので、設定値を動的に生成することも可能です。

“`javascript
// jest.config.js
module.exports = {
// テスト対象のファイル拡張子
moduleFileExtensions: [
‘js’,
‘json’,
‘jsx’,
‘ts’,
‘tsx’,
‘node’
],

// テストファイルのパターン
testMatch: [
/tests//.js?(x)’,
/?(.)+(spec|test).js?(x)’
],

// カバレッジ収集の対象ファイルと除外パターン
collectCoverageFrom: [
‘src//*.{js,jsx,ts,tsx}’,
‘!src/
/*.d.ts’, // 型定義ファイルは除外
‘!src/index.js’, // エントリーポイントなど、テストしないファイルを除外
],

// グローバルのカバレッジ閾値。これを下回るとテストが失敗する
coverageThreshold: {
‘global’: {
‘branches’: 75,
‘functions’: 80,
‘lines’: 80,
‘statements’: 80,
}
},

// モジュールのエイリアス設定 (Webpackのresolve.aliasに似ている)
moduleNameMapper: {
‘^@/(.*)$’: ‘/src/$1′,
‘\.css$’: ‘/mocks/styleMock.js’, // CSSファイルをモック化する場合
},

// テスト環境の設定
// ‘jsdom’はブラウザ環境をシミュレートする (DOM操作があるテスト向け)
// ‘node’はNode.js環境 (サーバーサイドのテスト向け)
testEnvironment: ‘jsdom’,

// 各テストファイルの前に実行されるセットアップファイル
// テスト環境の全体的な設定や、グローバルなjest mockingなどを記述
setupFilesAfterEnv: [
/jest.setup.js’, // 例: Testing LibraryのMatcher拡張など
],

// ファイルをJestが読み込む前に変換するための設定
// BabelやTypeScriptを使う場合に必要
transform: {
‘^.+\.(js|jsx|ts|tsx)$’: ‘babel-jest’, // BabelでJS/JSX/TS/TSXを変換
// ‘^.+\.css$’: ‘/node_modules/jest-css-modules’, // CSS Modulesを変換
},

// スナップショットシリアライザーの追加 (例: Enzyme, emotionなど)
// snapshotSerializers: [],

// テスト実行時の表示設定
verbose: true, // 詳細なテスト結果を表示
silent: false, // Jestの出力を非表示にしない

// モックの自動クリア
// 各テストの後にjest.clearAllMocks()を実行するのと同じ
clearMocks: true,

// テストが失敗した際にコンソールにインタラクティブなウォッチモードメニューを表示しない
// watchAll: false,
// watch: false,

// テストファイル以外のファイルも監視して、変更時にテストを再実行
// watchPathIgnorePatterns: [],

// テスト実行前に特定のモジュールをインポート
// setupFiles: [], // 環境全体で一度実行されるファイル
};
“`

12.3. 主要な設定オプションの解説

  • moduleFileExtensions, testMatch: Jestがテストファイルとして扱うファイルのパターンや拡張子を指定します。TypeScript (.ts, .tsx) を使う場合はこれらの拡張子を含める必要があります。
  • collectCoverageFrom: カバレッジを収集する対象のファイルを指定します。テストファイル自身や型定義ファイル(.d.ts)などは通常対象外とします。
  • coverageThreshold: グローバルまたはファイルごとのカバレッジの最低閾値を設定します。設定値を下回るとテスト実行が失敗します。
  • moduleNameMapper: モジュールのパス解決をカスタマイズします。Webpackなどで設定しているエイリアス(例: @/src/にマッピング)をJestでも認識させたい場合や、特定ファイルをモック化したい場合に利用します。
  • testEnvironment: テストを実行する環境を指定します。
    • node: Node.js環境。サーバーサイドや純粋なロジックのテストに。
    • jsdom: JSDOMを使用してブラウザのようなDOM環境をシミュレートします。ReactなどのUIコンポーネントテストによく使われます。
  • setupFilesAfterEnv: 各テストファイルが実行される「前」に実行されるセットアップファイルを指定します。テストユーティリティ(例: Testing LibraryのカスタムMatcher)のインポートや、テスト環境固有の初期化処理に利用します。
  • transform: テストファイルをJestが処理する前に、別のツール(Babel, ts-jestなど)で変換するための設定です。TypeScriptやJSX、新しいJavaScript構文を使用する場合に必須です。
  • clearMocks, resetMocks, restoreMocks: 各テスト後にモックの状態を自動的にリセットするかどうかを設定します。clearMocks: truebeforeEach(() => jest.clearAllMocks())と同じ効果があり、テスト間の副作用を防ぐために便利です。

これらの設定オプションを理解し、プロジェクトのニーズに合わせてカスタマイズすることで、Jestの機能を最大限に引き出し、効率的なテスト環境を構築できます。

13. 実践的なテクニックとヒント

Jestを使ったテスト開発をより効果的に行うための実践的なテクニックやヒントを紹介します。

13.1. 良いテストの名前付け

テストの名前(testdescribeの第一引数)は、そのテストが何を確認しているのかを明確に伝えるべきです。以下のパターンが推奨されます。

  • describe: テスト対象の「機能単位」や「モジュール」。例: 'User API', 'Authentication Service', 'ProductList Component'
  • test: テスト対象の「特定の状況」と「期待される結果」。例: 'should return user data when valid ID is provided', 'should throw error if password is too short', 'should display loading spinner while fetching data'

良いテスト名は、テストが失敗したときに、どの機能の、どのような状況で、何が期待通りでなかったのかをすぐに理解するのに役立ちます。

13.2. 単一責任の原則をテストにも適用

プロダクションコードと同様に、テストコードも単一責任の原則(SRP)に従うべきです。一つのテストケースは、一つのことだけをテストするべきです。これにより、テストが失敗したときに原因を特定しやすくなります。

複数のことを検証したい場合は、複数のexpectを使用するのではなく、複数のtestケースに分割することを検討しましょう。

13.3. テスト失敗時のデバッグ

テストが失敗した場合、Jestは失敗したテストの名前、発生したエラー、そして期待値と実際の値の差分を詳しく表示してくれます。

  • 差分を確認: Jestが表示する差分(diff)は、toEqualなどが失敗した場合に非常に役立ちます。オブジェクトや配列のどの部分が異なっているのかを確認しましょう。
  • console.logの活用: テストコードやテスト対象のコードにconsole.logを仕込んで、変数の値や処理の流れを確認できます。Jestはテスト実行時にconsole.logの出力をキャプチャして表示してくれます。
  • Jestのデバッグモード: JestをNode.jsのデバッガーと連携させて実行できます。node --inspect-brk node_modules/.bin/jest --runInBand [test-file] のようなコマンドで実行し、Chrome DevToolsなどから接続してステップ実行できます。--runInBandオプションは、テストを並列ではなく単一のプロセスで実行させるために必要です。
  • 特定のテストだけを実行: 失敗しているテストだけを繰り返し実行したい場合は、testdescribetest.onlydescribe.onlyに変更して実行します。デバッグが完了したら元に戻すのを忘れないようにしましょう。
    “`javascript
    describe(‘…’, () => {
    // 他のテストはスキップされる
    test.only(‘this test is failing’, () => {
    // …
    });

    test(‘this test will be skipped’, () => {
    // …
    });
    });
    “`

13.4. CI/CDパイプラインへの組み込み

自動テストは、継続的インテグレーション/継続的デリバリー(CI/CD)パイプラインに組み込むことで真価を発揮します。コードがリポジトリにプッシュされるたびに、自動的にJestテストが実行されるように設定しましょう。

  • GitHub Actions, GitLab CI, Jenkins, CircleCIなどのCIサービスは、Node.js環境でJestを実行する設定を容易に行えます。
  • テストが全て成功した場合のみ、次のステップ(ビルド、デプロイなど)に進むように設定します。
  • カバレッジレポートの生成をCIプロセスに含め、閾値を設定してコード品質を維持することも一般的です。カバレッジレポートをArtifactとして保存したり、Codecovなどのサービスと連携して可視化・追跡したりすることも可能です。

13.5. 他のツールとの連携

Jestは、JavaScript/TypeScript開発でよく使われる他のツールやライブラリと連携して使用されることが多いです。

  • Testing Library: UIコンポーネント(React, Vue, Angularなど)のテストにおいて、ユーザーの視点から要素を検索・操作するためのライブラリです。Jestと組み合わせて使われることが非常に多いです。JestのMatcherを拡張するjest-domもよく使われます。
  • Enzyme: Reactコンポーネントのテストライブラリ(Airbnb開発)。DOMツリーをより詳細に操作・検証したい場合に利用されます。最近はTesting Libraryに置き換えられる傾向があります。
  • Babel / TypeScript: これらのトランスパイラを使用している場合、Jestがテストを実行する前にコードを変換する必要があります。Jestの設定オプションtransformbabel-jestts-jestなどのトランスフォーマーを指定します。
  • ESLint / Prettier: コードスタイルや静的解析ツールと組み合わせることで、テストコードを含むプロジェクト全体の品質を向上させます。

13.6. 大規模プロジェクトでのJestの使い方

プロジェクトが大規模になると、テストケースの数も膨大になります。Jestは大規模プロジェクトにも対応できるよう設計されていますが、以下の点を考慮するとさらに効率的です。

  • テストファイルの分割: 機能単位やモジュールごとにテストファイルを細かく分割します。
  • 並列実行: Jestはデフォルトでテストを並列実行します。設定で並列度を調整することも可能です。
  • キャッシュ: Jestはテスト実行結果をキャッシュし、変更されたファイルに関連するテストだけを再実行する「watch mode」を提供しています。開発中はnpm test -- --watchnpm test -- --watchAllを活用しましょう。
  • 設定ファイルの管理: jest.config.jsを使用して、プロジェクト全体のテスト設定を一元管理します。複数のサブプロジェクトがある場合は、設定を共有したり、特定のディレクトリで異なる設定を適用したりすることも可能です。
  • カスタムコマンド: package.jsonのscriptsに、特定のテストスイートだけを実行するコマンド(例: test:unit, test:integration, test:watch)を追加しておくと便利です。

json
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest -- test/**/*.unit.js", // 特定のパターンに一致するテストのみ実行
"test:integration": "jest -- test/**/*.integration.js"
}

これらのテクニックやヒントを活用することで、Jestを使ったテスト開発をより効率的、効果的、そして持続可能なものにできます。

14. まとめ

この記事では、JavaScriptテストの重要性から始まり、Jestがなぜ現代のJavaScript開発においてテストのデファクトスタンダードとなりつつあるのか、その特徴を詳しく解説しました。さらに、Jestの導入方法から、アサーション、テスト構成、セットアップ/ティアダウン、モック、非同期テスト、スナップショットテスト、カバレッジレポート、設定オプション、そして実践的なテクニックに至るまで、Jestを使い始める上で、そして使いこなす上で必要な知識を網羅的に紹介しました。

Jestの「ゼロコンフィグ」で始められる手軽さ、高速な実行、オールインワンの機能、そして強力なモック機能は、JavaScriptコードの品質を向上させ、開発効率を高める上で大きな助けとなります。テストを開発プロセスに組み込むことは、バグの早期発見、リファクタリングの安全性確保、コードの品質向上、そして最終的にはより信頼性の高いソフトウェアを迅速に提供することにつながります。

この記事で紹介した内容は、Jestの世界への入門に過ぎません。Jestには他にも高度な機能や、様々なユースケースに対応するための豊富なオプションが用意されています。Jestの公式ドキュメントは非常に充実しており、学習を進める上で最も信頼できるリソースです。

ぜひ、今日からJestを使ってJavaScriptコードのテストを始めてみてください。テストを書く習慣を身につけることで、あなたの開発スキルは間違いなく向上し、より安心して、より楽しく開発を進められるようになるでしょう。Jestという強力なツールを味方につけて、堅牢で高品質なJavaScriptアプリケーション開発を目指しましょう!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール