Jestとは?JavaScript単体テスト入門

はい、承知いたしました。Jestを使ったJavaScript単体テスト入門に関する約5000語の詳細な記事を作成します。記事はMarkdown形式で直接表示します。


Jestとは?JavaScript単体テスト入門

ソフトウェア開発において、品質保証は非常に重要なプロセスです。その中でも、テストはバグを早期に発見し、コードの信頼性を高め、将来の変更に対する自信を与えてくれる不可欠な活動です。JavaScript開発の世界でも、フロントエンドからバックエンド(Node.js)まで、さまざまな場面でテストが活用されています。

本記事では、JavaScriptのテストの中でも特に「単体テスト(Unit Test)」に焦点を当て、そのための強力なフレームワークである「Jest」について、初心者の方でも理解できるように詳細に解説していきます。

この記事を読むことで、以下の内容が理解できるはずです。

  • なぜソフトウェアのテストが必要なのか
  • 単体テストとは何か、その重要性
  • Jestとは何か、その特徴と利点
  • Jestの基本的な使い方(インストールからテスト実行まで)
  • テストの書き方(describe, it/test, expect, マッチャー)
  • 非同期コードのテスト方法
  • モックの概念とJestでの実践方法
  • テストの組織化と共通処理(beforeEach, afterEachなど)
  • カバレッジレポートの見方
  • テストを書く上でのベストプラクティス

さあ、Jestの世界へ飛び込み、より堅牢なJavaScriptコードを書くための一歩を踏み出しましょう。

1. なぜソフトウェアのテストが必要なのか?

コードを書く際に「テストを書くのは面倒だ」「時間がかかる」と感じる方もいるかもしれません。しかし、テストは開発プロセスにおいて非常に大きな価値をもたらします。

バグの早期発見

これはテストの最も直接的な利点です。テストを書くことで、開発中のコードに潜むバグを、ユーザーの手元に届く前に発見できます。早い段階でバグを見つけるほど、修正コストは劇的に低くなります。リリース後にユーザーが発見するバグは、信頼性の低下にもつながります。

リファクタリングの安全性

コードは時間と共に変化し、改善(リファクタリング)が必要になります。しかし、リファクタリングは意図しない副作用(新たなバグ)を生むリスクを伴います。しっかりとしたテストスイート(テストの集合体)があれば、リファクタリングを行った後にテストを実行するだけで、既存の機能が壊れていないことを高い確度で確認できます。これにより、安心してコードを改善できるようになります。

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

テストは、コードが「何をすべきか」を明確に定義する役割も果たします。テストコードを読むことで、その関数やモジュールがどのような入力に対してどのような出力を期待するのか、どのような振る舞いをすべきなのかが理解できます。これは生きたドキュメンテーションとしても機能します。

設計の改善

テストしやすいコードは、一般的にモジュール性が高く、依存関係が整理されている傾向があります。テストを書く過程で、「この部分はテストしにくいな」と感じたら、それは設計を見直す良い機会かもしれません。テスト駆動開発(TDD)のように、テストを先に書く手法は、自然と良い設計を促します。

チーム開発での信頼性向上

チームで開発している場合、他のメンバーが書いたコードを変更したり、自分が書いたコードを他のメンバーが変更したりすることが頻繁にあります。テストがあれば、他のメンバーの変更によって自分の担当部分が壊れていないか、あるいは自分の変更が他の部分に影響を与えていないかを自動的に確認できます。これはチーム全体の生産性と信頼性を向上させます。

まとめ:テストは未来の自分への投資

テストを書くことは、短期的な時間投資です。しかし、その投資は長期的に見て、バグ修正にかかるコスト削減、開発スピードの向上、コードの保守性の向上、そして何よりも開発者の安心感という形で大きなリターンをもたらします。特に単体テストは、最も細かい粒度でのテストであり、バグをピンポイントで特定しやすいという利点があります。

2. テストの種類

ソフトウェアテストには様々なレベルと種類があります。代表的なものをいくつか紹介します。

  • 単体テスト (Unit Test): プログラムの最小単位(関数、メソッド、クラスなど)が意図した通りに動作するかを確認するテストです。最も粒度が小さく、高速に実行できます。この記事の主題です。
  • 結合テスト (Integration Test): 複数の単体(モジュールやコンポーネント)を組み合わせて、それらが正しく連携して動作するかを確認するテストです。
  • E2Eテスト (End-to-End Test): システム全体をユーザー視点で操作し、最初から最後まで(エンド・ツー・エンドで)意図した通りに動作するかを確認するテストです。例えば、Webアプリケーションであれば、ブラウザ上でユーザー操作(ボタンクリック、フォーム入力など)をシミュレートし、期待する結果が得られるかを確認します。
  • 受け入れテスト (Acceptance Test): システムがユーザーや顧客の要求(仕様)を満たしているかを確認するテストです。
  • パフォーマンステスト (Performance Test): システムが特定の負荷の下でどれだけ迅速に応答するか、安定して動作するかなどを評価するテストです。

これらのテストは、それぞれ異なる目的と粒度を持ち、組み合わせて行うことでより網羅的に品質を保証できます。単体テストは、これらのテストピラミッドの土台となる非常に重要な層です。

3. Jestとは何か?

Jestは、Facebookが開発したJavaScriptのテスティングフレームワークです。Reactアプリケーションのテストによく使われますが、Reactに限らず、Vue、Angular、Node.js、TypeScriptなど、様々なJavaScript/TypeScriptプロジェクトで広く利用されています。

JestがJavaScriptテスティングフレームワークとして非常に人気がある理由をいくつか挙げます。

特徴と利点

  1. オールインワン (Batteries Included): Jestは、テストランナー、アサーションライブラリ(expect)、モッキングライブラリ、カバレッジレポート機能などを内包しています。多くの機能が最初から揃っているため、別途様々なライブラリを組み合わせる必要がありません。これにより、セットアップが非常に簡単です。
  2. シンプルな設定: 多くのプロジェクトでは、追加の設定ファイルなしにJestをすぐに使い始めることができます。設定が必要な場合でも、直感的で分かりやすいオプションが用意されています。
  3. 高速なテスト実行: Jestはテストを並列に実行することで、大規模なプロジェクトでも高速なテストフィードバックを提供します。ファイル変更を検知して関連するテストだけを再実行する「watchモード」も非常に便利です。
  4. 強力なアサーションライブラリ (expect): 豊富な「マッチャー」が用意されており、様々な種類の検証を直感的に記述できます。
  5. 組み込みのモッキング機能: 外部の依存関係(関数、モジュールなど)を簡単に置き換える(モックする)機能が強力です。これにより、テスト対象のユニットだけを分離してテストしやすくなります。
  6. スナップショットテスト: UIコンポーネントやデータの構造が予期せず変更されていないかを簡単に確認できる機能です。
  7. 優れたドキュメンテーション: 公式ドキュメントが非常に充実しており、必要な情報を簡単に見つけられます。

これらの特徴から、Jestは特に初心者にとって導入しやすく、かつプロダクションレベルの開発にも十分に耐えうる強力なテスティングフレームワークと言えます。

4. Jestを使ってみよう:環境構築

Jestを使うための最初のステップは、プロジェクトへの導入です。Node.jsとnpmまたはyarnがインストールされていることを前提とします。

プロジェクトの準備

Jestを導入したいJavaScriptプロジェクトのルートディレクトリに移動します。もし新しいプロジェクトで始める場合は、以下のコマンドでpackage.jsonファイルを作成します。

bash
npm init -y

または

bash
yarn init -y

Jestのインストール

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

bash
npm install --save-dev jest

または

bash
yarn add --dev jest

これで、Jestがプロジェクトにインストールされました。

package.jsonにテストスクリプトを追加

Jestをコマンドラインから簡単に実行できるように、package.jsonscriptsセクションにテストコマンドを追加するのが一般的です。

package.jsonを開き、"scripts"の項目に以下を追加または変更します。

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

これで、ターミナルからnpm testまたはyarn testと入力するだけでJestを実行できるようになります。

最初のテスト対象コード

テスト対象となる簡単なJavaScriptファイルを作成してみましょう。例えば、二つの数値を合計する関数を持つファイルを作成します。

sum.js

javascript
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum; // Node.js環境を想定

もしES Modules(import/export)を使用している場合は、トランスパイラ(Babelやswc)の設定が必要になることがあります。Jest v22以降は、基本的なimport/exportは設定なしでも動作することが多いですが、複雑な場合は設定が必要です。簡単な例として、ここではNode.js標準のmodule.exportsを使用します。

最初のテストファイル

テスト対象ファイルに対応するテストファイルを作成します。Jestはデフォルトで以下の命名規則のファイルをテストファイルとして認識します。

  • .test.js
  • .spec.js
  • __tests__ ディレクトリ内の .js ファイル

ここでは、sum.test.jsというファイルを作成します。

sum.test.js

“`javascript
// sum.test.js
const sum = require(‘./sum’); // テスト対象の関数をインポート

test(‘adds 1 + 2 to equal 3’, () => {
expect(sum(1, 2)).toBe(3);
});
“`

このコードについて見ていきましょう。

  • const sum = require('./sum');: テスト対象のsum関数をインポートしています。
  • test('adds 1 + 2 to equal 3', () => { ... });: これがテストケース(単体テスト)を定義する部分です。
    • 第一引数の文字列 ('adds 1 + 2 to equal 3') は、このテストが何を確認しているのかを示す説明文です。分かりやすい名前をつけましょう。
    • 第二引数は、実際のテストロジックを含む関数です。
  • expect(sum(1, 2)): これは「アサーション」または「検証」の始まりです。sum(1, 2)という式の評価結果(この場合は3)をJestのexpect関数に渡しています。expectは、検証可能な「期待値」をラップするオブジェクトを返します。
  • .toBe(3): これは「マッチャー」と呼ばれるものです。expectでラップされた値(sum(1, 2)の結果である3)が、マッチャーの引数に渡された値(3)と等しいことを検証しています。toBeは厳密な等価性 (===) をチェックします。

テストの実行

これで準備が整いました。ターミナルで以下のコマンドを実行してください。

bash
npm test

または

bash
yarn test

コマンドを実行すると、Jestがプロジェクト内のテストファイルを検索し、実行します。成功すると、以下のような出力が表示されるはずです。

“`
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

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

PASSと表示され、テストが1つ成功したことがわかります。これで、Jestの基本的なセットアップとテストの実行方法が理解できたでしょう。

もしテストが失敗した場合、Jestは詳細なエラーメッセージと、期待していた値(Expected)と実際の結果(Received)を表示してくれます。例えば、sum.test.jsを以下のように変更して実行してみてください。

“`javascript
// 失敗する例
const sum = require(‘./sum’);

test(‘adds 1 + 2 to equal 3’, () => {
expect(sum(1, 2)).toBe(4); // 期待値を間違える
});
“`

実行結果は以下のようになります。

“`
FAIL ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

● adds 1 + 2 to equal 3

expect(received).toBe(expected) // Object.is equality

Expected: 4
Received: 3

at Object.<anonymous> (__tests__/sum.test.js:4:18)

Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: …s
Ran all test suites.
“`

FAILが表示され、どのテストが失敗したか、そしてExpected4だったのに対し、Received3だったことが明確に示されています。この情報はバグの原因特定に役立ちます。

5. Jestのコアコンセプト

Jestでテストを書く際に頻繁に使用する主要な要素について詳しく見ていきましょう。

describe(name, fn)

describe関数は、関連する複数のテストケースをグループ化するために使用します。これにより、テストコードの可読性と構成を向上させることができます。

“`javascript
// calculator.js
function add(a, b) { return a + b; }
function subtract(a, b) { return a – b; }

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

“`javascript
// calculator.test.js
const { add, subtract } = require(‘./calculator’);

describe(‘Calculator basic operations’, () => {
test(‘should add two numbers correctly’, () => {
expect(add(5, 3)).toBe(8);
});

test(‘should subtract two numbers correctly’, () => {
expect(subtract(10, 4)).toBe(6);
});
});
“`

describeブロックはネストすることも可能です。例えば、計算機に掛け算や割り算の機能を追加し、それぞれの操作をさらに細かくグループ化したい場合などに便利です。

“`javascript
// calculator.test.js (ネストされた describe の例)
const { add, subtract, multiply, divide } = require(‘./calculator’); // 仮に追加

describe(‘Calculator operations’, () => {

describe(‘Addition’, () => {
test(‘should add positive numbers’, () => {
expect(add(5, 3)).toBe(8);
});
test(‘should add negative numbers’, () => {
expect(add(-1, -5)).toBe(-6);
});
test(‘should add a positive and a negative number’, () => {
expect(add(10, -4)).toBe(6);
});
});

describe(‘Subtraction’, () => {
test(‘should subtract positive numbers’, () => {
expect(subtract(10, 4)).toBe(6);
});
test(‘should subtract with negative result’, () => {
expect(subtract(5, 8)).toBe(-3);
});
});

// 他の操作(Multiply, Divideなど)も同様に describe でグループ化
});
“`

describeを使うことで、テストレポートも構造化されて表示され、どの部分のテストが成功/失敗したのかが一目で分かりやすくなります。

it(name, fn) / test(name, fn)

ittestは完全に同じエイリアスです。単一のテストケースを定義するために使用します。どちらを使うかは個人の好みやプロジェクトのコーディング規約によりますが、一般的にはtestがよく使われます。

javascript
test('説明: テスト対象の関数が〇〇の場合、△△な結果を返すこと', () => {
// テストロジック
});

説明文は、「should 動詞 期待する結果 when 条件」のような形式で書くと、そのテストの目的が明確になります。(例: should return true when input is valid

expect(value) と マッチャー (Matchers)

expect(value)は、検証したい値を引数に取り、マッチャーを呼び出すためのオブジェクトを返します。マッチャーは、その値が特定の条件を満たすかどうかを検証するメソッドです。

Jestは非常に多くの種類のマッチャーを提供しています。主要なものをいくつか紹介します。

等価性の検証

  • toBe(expected): 厳密な等価性 (===) をチェックします。主にプリミティブ値(文字列、数値、真偽値など)に使います。
  • toEqual(expected): 値の等価性(Deep equality)をチェックします。オブジェクトや配列の中身が同じであるかを再帰的に比較します。オブジェクトや配列の比較にはこちらを使います。

“`javascript
test(‘toBe and toEqual examples’, () => {
// toBe: プリミティブの比較
expect(1 + 1).toBe(2);
expect(‘hello’).toBe(‘hello’);
expect(true).toBe(true);

// toBe: オブジェクトは参照が同じでないと失敗
const obj1 = { a: 1 };
const obj2 = { a: 1 };
// expect(obj1).toBe(obj2); // 失敗する

// toEqual: オブジェクトや配列の中身の比較
expect(obj1).toEqual(obj2); // 成功する
expect([1, 2, 3]).toEqual([1, 2, 3]); // 成功する
});
“`

真偽値の検証

  • toBeNull(): 値がnullであるかをチェックします。
  • toBeUndefined(): 値がundefinedであるかをチェックします。
  • toBeDefined(): 値がundefinedではないかをチェックします。
  • toBeTruthy(): 値が論理的に真(true)と評価されるか(JavaScriptのtruthyな値、例: 1, “hello”, true, {}, []など)をチェックします。
  • toBeFalsy(): 値が論理的に偽(false)と評価されるか(JavaScriptのfalsyな値、例: 0, “”, null, undefined, false, NaNなど)をチェックします。

“`javascript
test(‘truthiness examples’, () => {
const n = null;
const u = undefined;
const zero = 0;
const emptyString = ”;
const obj = {};

expect(n).toBeNull();
expect(u).toBeUndefined();
expect(zero).toBeFalsy();
expect(emptyString).toBeFalsy();
expect(obj).toBeTruthy();
expect(n).toBeFalsy(); // null は falsy
expect(zero).toBeDefined(); // 0 は defined
});
“`

数値の検証

  • toBeGreaterThan(number): より大きいか
  • toBeGreaterThanOrEqual(number): 以上か
  • toBeLessThan(number): より小さいか
  • toBeLessThanOrEqual(number): 以下か
  • toBeCloseTo(number, numDigits): 浮動小数点数の等価性を、指定された桁数(numDigits、デフォルトは2)で検証します。浮動小数点数の計算誤差に対応するため、toBeの代わりにこちらを使うのが一般的です。

“`javascript
test(‘number matchers’, () => {
const value = 0.1 + 0.2; // 0.30000000000000004

// expect(value).toBe(0.3); // 浮動小数点誤差で失敗する可能性がある
expect(value).toBeCloseTo(0.3); // 成功する

expect(10).toBeGreaterThan(5);
expect(7).toBeLessThan(10);
expect(8).toBeGreaterThanOrEqual(8);
expect(8).toBeLessThanOrEqual(8);
});
“`

文字列の検証

  • toMatch(regexpOrString): 文字列が正規表現または部分文字列にマッチするかをチェックします。

javascript
test('string matchers', () => {
expect('team').toMatch(/tea/);
expect('hello world').toMatch('world');
});

配列・Iterableの検証

  • toContain(item): 配列やIterableに特定の要素が含まれているかをチェックします。

javascript
test('array matchers', () => {
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];
expect(shoppingList).toContain('milk');
expect(new Set(['apple', 'banana'])).toContain('apple');
});

例外の検証

  • toThrow(error?): 関数が実行されたときにエラーをスローすることを期待する場合に使用します。引数に文字列、正規表現、エラーインスタンス、またはエラークラスを渡すことで、スローされたエラーの詳細を検証できます。

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

test(‘compiling android goes as expected’, () => {
// エラーがスローされるかだけをチェック
expect(() => compileAndroidCode()).toThrow();

// エラーメッセージをチェック
expect(() => compileAndroidCode()).toThrow(‘you are using the wrong JDK’);
expect(() => compileAndroidCode()).toThrow(/JDK/); // 正規表現

// 特定のエラークラスをチェック
// expect(() => { throw new MyError(); }).toThrow(MyError); // MyErrorというカスタムエラークラスがある場合
});
“`

toThrowを使用する際は、検証対象のコードを関数としてラップする必要がある点に注意してください(例: () => compileAndroidCode())。これは、Jestがアサーションを実行する前に、エラーをスローする可能性のあるコードを実行する必要があるためです。もしexpect(compileAndroidCode())と書いてしまうと、Jestがexpectを処理する前にcompileAndroidCode()が実行されてエラーがスローされ、テストフレームワークがエラーを捕捉できずにテストが失敗してしまいます。

.not 否定のマッチャー

ほとんどのマッチャーは.notプロパティと組み合わせて、その逆の条件を検証できます。

javascript
test('not examples', () => {
expect(1 + 1).not.toBe(3);
expect({ a: 1 }).not.toEqual({ b: 2 });
expect('hello').not.toMatch(/bye/);
});

これらのマッチャーを組み合わせることで、様々な条件下での関数の振る舞いを詳細に検証できます。テストを書く際は、対象となるコードがとりうる様々な入力値や状態を想定し、それぞれに対応する検証を行うことが重要です。

6. 非同期コードのテスト

JavaScriptでは、Promise、async/await、コールバックなど、非同期処理が頻繁に登場します。API呼び出し、ファイル読み込み、タイマー処理などがこれにあたります。Jestは非同期コードのテストもサポートしています。

非同期テストで最も重要なのは、「非同期処理が完了するまでテストランナーが待機する」ように Jest に伝えることです。待たずにテスト関数が終了してしまうと、非同期処理の結果を検証する前にテストが成功または失敗と判断されてしまいます。

Jestで非同期コードをテストする方法はいくつかあります。

コールバック (done)

これは非同期テストの最も古い方法ですが、互換性のために知っておくと良いでしょう。テスト関数の引数にdoneという名前のコールバック関数を指定します。そして、非同期処理が完了し、アサーションが実行された後にdone()を呼び出すことで、Jestにテストが完了したことを伝えます。

javascript
// fetchData.js (仮)
function fetchData(callback) {
setTimeout(() => {
callback('peanut butter');
}, 1000);
}
module.exports = fetchData;

“`javascript
// fetchData.test.js
const fetchData = require(‘./fetchData’);

test(‘the data is peanut butter’, (done) => { // done を引数で受け取る
function callback(data) {
try {
expect(data).toBe(‘peanut butter’);
done(); // 非同期処理と検証が完了したら done() を呼ぶ
} catch (error) {
done(error); // エラーが発生したら done(error) を呼んでテスト失敗を伝える
}
}

fetchData(callback);
});
“`

この方法では、非同期処理内で発生したエラーをtry...catchで捕捉し、done(error)として渡すことでJestにエラーを知らせる必要があります。少し冗長になるのが欠点です。

Promises

テスト関数からPromiseを返すことで、JestはそのPromiseが解決(resolve)または拒否(reject)されるまで待機します。

javascript
// fetchDataPromise.js (仮)
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('peanut butter');
}, 1000);
});
}
module.exports = fetchDataPromise;

“`javascript
// fetchDataPromise.test.js

const fetchDataPromise = require(‘./fetchDataPromise’);

// Promise が解決されることを検証
test(‘the data is peanut butter (Promise .then)’, () => {
return fetchDataPromise().then(data => { // Promise を返す
expect(data).toBe(‘peanut butter’);
});
});

// Promise が拒否されることを検証 (エラーハンドリング)
test(‘the fetch fails with an error (Promise .catch)’, () => {
// 失敗する Promise を返す関数 (仮)
function fetchDataWithError() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(‘error’);
}, 1000);
});
}

expect.assertions(1); // Promise が reject された場合にテストが必ず実行されることを保証
return fetchDataWithError().catch(e => expect(e).toMatch(‘error’));
});
“`

Promiseを返す場合、特に.catchでエラーを検証するテストでは、expect.assertions(number)を使うことが推奨されます。これは、Promiseが意図せず解決されてしまった場合などに、catchブロック内のexpectが実行されずにテストが成功と判断されてしまうのを防ぐためです。expect.assertions(1)は、このテストの中で少なくとも1回のアサーション(expect(...))が実行されることをJestに伝えます。指定した回数だけアサーションが実行されなかった場合、テストは失敗します。

Jestは.resolves.rejectsという便利なマッチャーも提供しています。これを使うと、より簡潔にPromiseの結果を検証できます。

“`javascript
// Promise の解決を検証 (.resolves)
test(‘the data is peanut butter (.resolves)’, () => {
return expect(fetchDataPromise()).resolves.toBe(‘peanut butter’);
});

// Promise の拒否を検証 (.rejects)
test(‘the fetch fails with an error (.rejects)’, () => {
// 失敗する Promise を返す関数 (仮)
function fetchDataWithError() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(‘error’);
}, 1000);
});
}
return expect(fetchDataWithError()).rejects.toMatch(‘error’);
});
“`

.resolves.rejectsを使う場合、expect.assertions(number)は不要です。

async/await

Promiseを使う最も現代的で可読性の高い方法です。asyncキーワードをテスト関数に付け、非同期処理の前にawaitキーワードを置くことで、Promiseが解決されるのを待つことができます。

javascript
// fetchDataPromise.js (上記と同じ)
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('peanut butter');
}, 1000);
});
}
module.exports = fetchDataPromise;

“`javascript
// fetchDataPromise.test.js (async/await)

const fetchDataPromise = require(‘./fetchDataPromise’);

test(‘the data is peanut butter (async/await)’, async () => { // async キーワード
const data = await fetchDataPromise(); // await で Promise が解決されるのを待つ
expect(data).toBe(‘peanut butter’);
});

test(‘the fetch fails with an error (async/await)’, async () => { // async キーワード
// 失敗する Promise を返す関数 (仮)
function fetchDataWithError() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(‘error’);
}, 1000);
});
}

try {
await fetchDataWithError(); // await で Promise が拒否されるのを待つ
} catch (e) {
expect(e).toMatch(‘error’);
}
});
“`

async/awaitは、非同期処理をまるで同期処理のように書けるため、テストコードが非常に読みやすくなります。非同期テストを書く際は、基本的にこの方法を使うのが最も推奨されます。エラーの検証にはtry...catchブロックを使用します。

非同期テストは、特に外部サービスへのAPIコールやデータベース操作を含むコードのテストで非常に重要になります。しかし、これらの実際の外部依存はテストを遅くし、外部要因による不安定さをもたらす可能性があります。そこで次のトピックである「モック」が重要になります。

7. モック (Mocking)

単体テストの目的は、テスト対象のユニット(関数、クラスなど)を独立してテストすることです。しかし、多くのユニットは他のモジュール、外部API、データベースなど、さまざまな「依存関係」を持っています。これらの依存関係がテストの邪魔になることがあります。

  • テストが遅くなる: 外部APIへのネットワークリクエストは時間がかかります。
  • テストが不安定になる: 外部サービスのダウン、ネットワークの遅延、テスト環境でのデータ不整合などにより、テストが失敗する可能性があります。これは「不安定なテスト(flaky test)」と呼ばれ、信頼性を損ないます。
  • テストシナリオの再現が難しい: 特定のエラーレスポンスや、特殊なデータを返すシナリオを再現するために、実際の依存関係を用意するのは困難な場合があります。

ここで「モック(Mock)」の概念が登場します。モックとは、テスト対象のユニットが依存している他の要素を、コントロール可能な「偽物(フェイク)」に置き換えることです。これにより、テスト対象のユニットを依存関係から切り離し、そのユニット自身のロジックだけを独立して、かつ高速かつ安定してテストできるようになります。

Jestは強力なモッキング機能を組み込んでいます。

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

Jestで関数をモックする最も基本的な方法は、jest.fn()を使用することです。これは「モック関数(Mock Function)」を作成します。モック関数は、それがどのように呼び出されたか(引数、呼び出し回数など)を記録し、戻り値を指定したり、カスタムの実装を提供したりできます。

``javascript
// 例えば、このユーティリティ関数をテストしたいが、内部で他の関数を使っている
// utils.js
function greet(name) {
return
Hello, ${name}!`;
}

function sayGoodbye(name) {
// 実際のテストでは呼びたくないかもしれない関数
console.log(Goodbye, ${name});
return Goodbye, ${name};
}

function complexFunction(name) {
const greeting = greet(name); // 依存関数
const farewell = sayGoodbye(name); // 依存関数
return ${greeting} ${farewell};
}

module.exports = { greet, sayGoodbye, complexFunction };
“`

complexFunctionをテストしたいが、console.logがテスト実行時に出力されるのを避けたい、あるいはsayGoodbyeの内部ロジックが複雑でテスト時間を遅くする場合などを考えます。

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

describe(‘complexFunction’, () => {
// sayGoodbye 関数をモック
const mockSayGoodbye = jest.fn();

// complexFunction が sayGoodbye のモックを使用するように置き換える (一時的)
// test の前に実行される beforeEach で行うのが一般的
let originalSayGoodbye;
beforeEach(() => {
// 元の関数を保存
originalSayGoodbye = utils.sayGoodbye;
// sayGoodbye をモック関数で置き換える
utils.sayGoodbye = mockSayGoodbye;

  // モック関数の呼び出し履歴や戻り値をリセット
  mockSayGoodbye.mockClear();

});

afterEach(() => {
// テスト後に元の関数に戻す
utils.sayGoodbye = originalSayGoodbye;
});

test(‘should call greet and the mocked sayGoodbye’, () => {
// greet 関数はモックしない (必要ならモックしても良い)
const result = utils.complexFunction(‘Alice’);

// complexFunction が sayGoodbye を呼び出したか検証
expect(mockSayGoodbye).toHaveBeenCalled(); // 少なくとも1回呼び出されたか
expect(mockSayGoodbye).toHaveBeenCalledTimes(1); // ちょうど1回呼び出されたか
expect(mockSayGoodbye).toHaveBeenCalledWith('Alice'); // 指定した引数で呼び出されたか

// sayGoodbye のモックの戻り値を指定する場合
mockSayGoodbye.mockReturnValue('Mocked Goodbye!');
const resultWithMockReturn = utils.complexFunction('Bob');
expect(resultWithMockReturn).toBe('Hello, Bob! Mocked Goodbye!');

// greet 関数の結果と sayGoodbye のモックの結果が結合されたことを検証 (元のコードでは sayGoodbye の戻り値は使ってないが例として)
// expect(result).toBe('Hello, Alice! Goodbye, Alice'); // sayGoodbye の実際の戻り値が使われないため、検証は complexFunction の期待される結果に依存する

});

// complexFunction が sayGoodbye のモックを使うことの具体的な検証
test(‘should return correctly formatted string using mocked sayGoodbye’, () => {
// sayGoodbye のモックの戻り値を設定
mockSayGoodbye.mockReturnValue(‘Fake Farewell’);

  const result = utils.complexFunction('Charlie');

  // complexFunction の内部ロジックに基づいた検証
  // complexFunction は greet の結果 + sayGoodbye の結果を返す想定
  // この例では sayGoodbye の結果を complexFunction が使っていないので、
  // ここでの検証は complexFunction が greet を呼び出しているか、
  // そして複雑なロジック(ここでは文字列結合)が正しく行われているかに焦点を当てるべき
  // 例をシンプルにするため、sayGoodbye が使われる想定で進める
  expect(result).toBe('Hello, Charlie! Fake Farewell'); // greet の結果 + モックの結果
  expect(mockSayGoodbye).toHaveBeenCalledWith('Charlie'); // sayGoodbye が正しい引数で呼ばれたか

});
});
“`

モック関数は、以下のような便利なプロパティとメソッドを持っています。

  • .mock.calls: モック関数が呼び出されたときの引数の配列を格納します。
  • .mock.results: モック関数の各呼び出しの結果(戻り値やスローされたエラー)を格納します。
  • .mock.instances: モック関数がコンストラクタとして呼び出された場合のインスタンスを格納します。
  • .mockClear(): .mock.calls, .mock.results, .mock.instances をリセットします。
  • .mockReset(): .mockClear() に加えて、モック関数の実装や戻り値の設定もリセットします。
  • .mockRestore(): .mockReset() に加えて、元の実装があればそれに戻します。

モジュール全体のモック (jest.mock())

外部モジュール(npmパッケージ、自作モジュール)全体をモックしたい場合に使用します。例えば、APIクライアントやデータベースアクセスクラスなど、実際の処理に依存しないようにテストしたい場合に便利です。

jest.mock(moduleName, factoryFunction) は、テストファイルのスコープ内で特定のモジュールを置き換えます。通常、テストファイルの先頭で呼び出します。

javascript
// api.js (仮)
async function fetchUser(userId) {
// 実際のAPIコールロジック(ネットワークリクエストなど)
console.log(`Fetching user ${userId} from actual API...`);
// return fetch(`/api/users/${userId}`).then(res => res.json());
return Promise.resolve({ id: userId, name: `User ${userId}` }); // サンプルなので Promise で返す
}
module.exports = { fetchUser };

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

async function getUserProfile(userId) {
const user = await api.fetchUser(userId);
return {
id: user.id,
name: user.name.toUpperCase(),
profileUrl: /users/${user.id}
};
}

module.exports = { getUserProfile };
“`

userService.getUserProfileをテストしたいが、実際のapi.fetchUser(ネットワークリクエスト)は実行したくない場合。

“`javascript
// userService.test.js

// api モジュール全体をモックする
// 第二引数に関数ファクトリを渡すと、その戻り値がモジュールの中身として使われる
jest.mock(‘./api’, () => ({
// fetchUser 関数をモック関数で置き換える
fetchUser: jest.fn(),
}));

const userService = require(‘./userService’);
// jest.mock の呼び出しは require の前に記述する必要がある

// モックされた api モジュールを取得 (as const を使うと型安全性が向上)
const mockedApi = require(‘./api’);
// または import 文を使っているなら
// import * as api from ‘./api’;
// const mockedApi = api; // あるいは Jest の自動モック機能を使っていれば直接 import した api がモックになる

// mockedApi.fetchUser は jest.fn() に置き換えられている
// モック関数の戻り値を設定
mockedApi.fetchUser.mockResolvedValue({ id: 1, name: ‘testuser’ }); // 非同期関数なので mockResolvedValue を使うと便利

describe(‘getUserProfile’, () => {
// 各テストケースの前にモックの状態をクリア
beforeEach(() => {
mockedApi.fetchUser.mockClear();
});

test(‘should fetch user and format profile’, async () => {
const userId = 1;
const expectedProfile = {
id: 1,
name: ‘TESTUSER’,
profileUrl: ‘/users/1’,
};

const profile = await userService.getUserProfile(userId);

// api.fetchUser が正しい引数で呼び出されたか検証
expect(mockedApi.fetchUser).toHaveBeenCalledTimes(1);
expect(mockedApi.fetchUser).toHaveBeenCalledWith(userId);

// getUserProfile が期待される値を返したか検証
expect(profile).toEqual(expectedProfile);

});

test(‘should handle error when fetching user’, async () => {
// fetchUser がエラーを reject するように設定
mockedApi.fetchUser.mockRejectedValue(new Error(‘API Error’));

  const userId = 2;

  // エラーがスローされることを検証
  await expect(userService.getUserProfile(userId)).rejects.toThrow('API Error');

  // fetchUser が正しい引数で呼び出されたか検証
  expect(mockedApi.fetchUser).toHaveBeenCalledTimes(1);
  expect(mockedApi.fetchUser).toHaveBeenCalledWith(userId);

});
});
“`

jest.mock()の第二引数にファクトリ関数を指定することで、モジュールの特定の部分だけをモックしたり、モックの実装を詳細に制御したりできます。ファクトリ関数は、そのモジュールがrequire()されたときに一度だけ実行され、その戻り値がモジュールとして使われます。

また、jest.mock()の第二引数を省略すると、Jestはモジュールを「自動モック」します。これは、モジュールの各エクスポート(関数、オブジェクトなど)を、元のエクスポートと同じシグネチャを持つjest.fn()に置き換えます。ただし、クラスや特定のケースでは意図したように動作しないこともあるため、明示的にファクトリ関数を指定する方が安全で分かりやすい場合が多いです。

スパイ (Spies)

Jestにはjest.spyOn()という機能もあります。これは関数やメソッドを「スパイ」するために使用します。スパイは、元の関数の実際の呼び出しを監視しつつ、その呼び出し履歴を記録します。これはモック関数 (jest.fn()) が呼び出し履歴を記録するのと似ていますが、スパイは元の関数を実行する点が異なります。

``javascript
// utils.js (上記と同じ)
function greet(name) {
return
Hello, ${name}!`;
}

function sayGoodbye(name) {
console.log(Goodbye, ${name});
return Goodbye, ${name};
}

function complexFunction(name) {
const greeting = greet(name);
const farewell = sayGoodbye(name);
return ${greeting} ${farewell};
}

module.exports = { greet, sayGoodbye, complexFunction };
“`

complexFunctionをテストする際に、greet関数が正しく呼ばれたかを監視したいが、実際のgreetの実装は実行したい場合。

“`javascript
// utils.test.js

const utils = require(‘./utils’);

describe(‘complexFunction with spyOn’, () => {
let greetSpy;

beforeEach(() => {
    // utils オブジェクトの greet メソッドをスパイする
    greetSpy = jest.spyOn(utils, 'greet');
    // spyOn は元の関数を実行する。
    // 必要に応じて mockImplementation や mockReturnValue で挙動を変えることも可能。
});

afterEach(() => {
    // スパイを元の関数に戻す (重要!)
    greetSpy.mockRestore();
    // または jest.restoreAllMocks(); ですべてのスパイ/モックを復元
});

test('should call greet with the correct argument', () => {
    const name = 'Alice';
    utils.complexFunction(name);

    // greet が呼び出されたか、引数は正しかったかを検証
    expect(greetSpy).toHaveBeenCalled();
    expect(greetSpy).toHaveBeenCalledTimes(1);
    expect(greetSpy).toHaveBeenCalledWith(name);

    // complexFunction の最終結果を検証 (元の greet 関数が実行されている)
    // complexFunction は sayGoodbye も呼ぶが、sayGoodbye はスパイしていないため実際に関数が実行される
    // console.log が出力される
    expect(utils.complexFunction(name)).toBe(`Hello, ${name}! Goodbye, ${name}`);
});

});
“`

jest.spyOn(object, methodName)は、指定されたオブジェクトの指定されたメソッドを監視します。元のメソッドは引き続き実行されますが、その呼び出しに関する情報は記録されます。spyOnによって作成されたスパイも、jest.fn()で作成されたモック関数と同じマッチャー(toHaveBeenCalledなど)を使用できます。

スパイは、特定のメソッドが呼び出されたかどうか、どの引数で呼び出されたかを検証したい場合に便利です。一方、モック関数 (jest.fn()) やモジュールモック (jest.mock()) は、依存関係の実際の挙動を置き換えたい場合に適しています。

モックやスパイは単体テストにおいて不可欠なテクニックです。これらを適切に使うことで、テスト対象のコードを分離し、より信頼性の高いテストを書くことができます。

8. テストの組織化とセットアップ・ティアダウン

プロジェクトが大きくなるにつれて、テストファイルの数も増えてきます。関連するテストをまとめたり、各テストの実行前後に共通のセットアップやクリーンアップ処理を行ったりすることが重要になります。

テストファイルの命名と配置

前述の通り、Jestはデフォルトで以下の命名規則のファイルをテストファイルとして認識します。

  • .test.js
  • .spec.js
  • __tests__ ディレクトリ内の .js ファイル

一般的には、テスト対象のファイルと同じディレクトリに.test.jsファイルを配置するか、プロジェクトルートに__tests__ディレクトリを作成し、その中にテスト対象のファイル構造をミラーリングしてテストファイルを配置するかのどちらかです。

例:
project-root/
├── src/
│ ├── utils.js
│ └── components/
│ └── Button.js
└── __tests__/
├── utils.test.js
└── components/
└── Button.test.js

または
project-root/
└── src/
├── utils.js
├── utils.test.js <-- テスト対象と同じディレクトリ
└── components/
├── Button.js
└── Button.test.js <-- テスト対象と同じディレクトリ

どちらの方法も有効ですが、プロジェクト内で一貫性を保つことが重要です。

セットアップとティアダウン

テストの実行前後に、共通の処理を実行したい場合があります。例えば、

  • テストデータを準備する
  • モックの状態をリセットする
  • データベース接続を確立・切断する
  • 特定の環境変数を設定・解除する

Jestはこれらの処理を定義するためのフック(Hook)を提供しています。これらのフックは、describeブロック内、またはグローバルスコープで使用できます。

  • beforeAll(fn, timeout): 現在のスコープ(describeブロックまたはファイル全体)内の全てのテストケースが実行されるに一度だけ実行されます。
  • afterAll(fn, timeout): 現在のスコープ内の全てのテストケースが実行されたに一度だけ実行されます。
  • beforeEach(fn, timeout): 現在のスコープ内のテストケースが実行されるに毎回実行されます。
  • afterEach(fn, timeout): 現在のスコープ内のテストケースが実行されたに毎回実行されます。

例:

“`javascript
// example.test.js

describe(‘Some feature’, () => {
let sharedData; // describe スコープで共有される変数

// この describe ブロックの全てのテストの前に一度だけ実行
beforeAll(() => {
console.log(‘beforeAll: Setting up shared resources’);
// 例: データベース接続など時間のかかるセットアップ
sharedData = [‘item1’, ‘item2’];
});

// この describe ブロックの全てのテストの後に一度だけ実行
afterAll(() => {
console.log(‘afterAll: Tearing down shared resources’);
// 例: データベース接続の切断
sharedData = null; // リソース解放のイメージ
});

// この describe ブロック内の各テストの前に毎回実行
beforeEach(() => {
console.log(‘ beforeEach: Preparing for a test’);
// 例: モックのリセット、テスト固有のデータの準備
});

// この describe ブロック内の各テストの後に毎回実行
afterEach(() => {
console.log(‘ afterEach: Cleaning up after a test’);
// 例: モックのクリア、一時ファイルの削除
jest.clearAllMocks(); // 全てのモック関数の状態をクリア
});

test(‘Test case 1’, () => {
console.log(‘ Executing Test 1’);
expect(sharedData).toEqual([‘item1’, ‘item2’]);
// テストロジック
});

test(‘Test case 2’, () => {
console.log(‘ Executing Test 2’);
expect(sharedData).toEqual([‘item1’, ‘item2’]);
// テストロジック
});
});

describe(‘Another feature’, () => {
// こちらの describe にも独自の before/after フックを設定可能
beforeAll(() => {
console.log(‘Another feature beforeAll’);
});
test(‘Another test case’, () => {
console.log(‘ Executing another test’);
});
});
“`

この例を実行すると、以下のような順序でログが表示されるはずです(実際の実行順序は並列実行の設定などにより多少異なる場合がありますが、フックの実行タイミングとスコープは保証されます)。

beforeAll: Setting up shared resources
beforeEach: Preparing for a test
Executing Test 1
afterEach: Cleaning up after a test
beforeEach: Preparing for a test
Executing Test 2
afterEach: Cleaning up after a test
afterAll: Tearing down shared resources
Another feature beforeAll
Executing another test
... (他のテストやフックの出力)

beforeEachafterEachは、各テストが独立して実行されるように、テスト間の副作用を防ぐのに役立ちます。例えば、あるテストでグローバルな設定を変更した場合、afterEachで元の状態に戻すことで、次のテストに影響を与えないようにできます。モックのクリアはafterEachで行うのが非常に一般的です (jest.clearAllMocks()や個別のモック関数の.mockClear())。

非同期のセットアップ/ティアダウンが必要な場合は、フック関数からPromiseを返したり、async/awaitを使用したりできます。

“`javascript
beforeAll(async () => {
await setupDatabase(); // 非同期セットアップ
});

afterEach(() => {
cleanupTestData(); // 同期クリーンアップ
});

afterAll(async () => {
await closeDatabase(); // 非同期ティアダウン
});
“`

適切にセットアップとティアダウンを行うことで、テストはより信頼性が高く、管理しやすくなります。

9. テストの実行オプションとウォッチャー

npm test または yarn test でJestを実行すると、通常はプロジェクト内の全てのテストファイルが実行されます。しかし、特定のテストだけを実行したい、特定のモードで実行したいといった場合があります。

コマンドラインオプション

Jestは様々なコマンドラインオプションを提供しています。

  • 特定のテストファイルの実行:
    bash
    npm test path/to/your/test-file.test.js
    # またはディレクトリを指定してその中の全てのテストを実行
    # npm test path/to/directory/
  • テスト名のフィルタリング (-t, –testNamePattern): テストの説明文(test()describe()の第一引数)の一部を指定して、マッチするテストのみを実行します。
    bash
    npm test -t 'should add two numbers'
  • テストスイートのフィルタリング (-f, –filter): ファイルパスの一部を指定して、マッチするファイルのみを実行します。
    bash
    npm test -f 'calculator' # ファイル名に 'calculator' を含むテストファイルを実行
  • 特定のテストだけを実行 (.only): コード内で一時的に特定のdescribeまたはtestだけに.onlyを付けることで、それ以外のテストをスキップして実行できます。デバッグ時に便利ですが、.onlyを付けたままコミットしないように注意が必要です。
    javascript
    describe.only('Focus on this feature', () => { ... });
    test.only('Focus on this test case', () => { ... });
  • 特定のテストをスキップ (.skip): .skipを付けることで、特定のdescribeまたはtestをスキップできます。まだ実装中の機能や、一時的にテストを実行したくない場合に利用します。
    javascript
    describe.skip('Skip this feature', () => { ... });
    test.skip('Skip this test case', () => { ... });
  • キャッシュの無効化 (–no-cache): Jestはテスト実行を高速化するためにキャッシュを使用しますが、まれにキャッシュが原因で問題が発生することがあります。その場合にキャッシュを無効にして実行します。
    bash
    npm test -- --no-cache
    # npm の場合は -- の後に Jest オプションを記述
    # yarn の場合はそのまま yarn test --no-cache
  • カバレッジレポートの生成 (–coverage): コードカバレッジレポートを生成します(後述)。
    bash
    npm test -- --coverage

ウォッチモード (--watchAll, --watch)

Jestの最も便利な機能の一つがウォッチャーです。テスト対象またはテストファイルに変更があった際に、自動的に関連するテストを再実行してくれます。これにより、コード変更→テスト実行→結果確認のサイクルが非常に素早く行えます。

  • npm test または yarn test を引数なしで実行すると、デフォルトでウォッチャーが起動することが多いです(package.jsonの設定によります)。
  • 明示的にウォッチャーを起動する場合:
    bash
    npm test -- --watchAll
    # yarn test --watchAll

ウォッチモード中にターミナルに表示されるメニュー(pでファイルフィルタ、tでテスト名フィルタなど)を使うと、実行するテストを絞り込むことができ、非常に効率的です。

10. コードカバレッジ

コードカバレッジ(Code Coverage)は、テストによってコードのどの部分が実行されたかを示す指標です。行数、分岐、関数、ステートメントなどの単位で測定されます。Jestは組み込みでコードカバレッジレポートの生成機能を持ちます。

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

テスト実行時に--coverageオプションを付け加えるだけです。

bash
npm test -- --coverage

実行が完了すると、以下のようなサマリーがターミナルに表示されます。

---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 80 | 50 | 75 | 80 |
sum.js | 100 | 100 | 100 | 100 |
calculator.js | 50 | 0 | 50 | 50 | 2,3
---------------|---------|----------|---------|---------|-------------------

これは各ファイルのステートメント(% Stmts)、分岐(% Branch)、関数(% Funcs)、行(% Lines)の実行率を示しています。Uncovered Line #sはテストで実行されなかった行番号を示します。

Jestはデフォルトでプロジェクトルートにcoverageディレクトリを作成し、HTML形式の詳細なレポートを生成します。coverage/lcov-report/index.htmlをブラウザで開くと、コードの各行が色分けされて表示され、テストで実行された行、実行されなかった行、分岐などが視覚的に確認できます。

カバレッジ率の解釈

コードカバレッジ率は、テストの網羅性を測るための一つの指標として有用ですが、カバレッジ率が高いからといって必ずしもテストの質が高いわけではないことに注意が必要です。

  • 高いカバレッジ率の利点: テストされていないコード部分を発見しやすくなります。リファクタリングや変更の影響範囲を把握するのに役立ちます。
  • カバレッジ率の限界: テストによってコードが実行されたとしても、その結果が正しく検証されているかはカバレッジでは分かりません。例えば、単に関数を呼び出すだけのテストでは、その関数が間違った結果を返してもカバレッジは100%になる可能性があります。

理想的には、単にコードを実行するだけでなく、考えられる様々な入力に対して期待される出力を正確に検証するテストを書くべきです。カバレッジレポートは、「どこにテストが足りないか」を知るためのツールとして活用し、単なる数字の目標として追い求めるべきではありません。

カバレッジの閾値設定

package.jsonのJest設定、またはjest.config.jsファイルで、最低限満たすべきカバレッジ率の閾値を設定できます。これにより、指定したカバレッジ率を下回った場合にテストを失敗させることができます。

json
// package.json の例
{
"jest": {
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": -10
},
"./src/components/": {
"branches": 90,
"functions": 90,
"lines": 90,
"statements": 90
}
}
}
}

このように設定することで、プロジェクト全体のカバレッジ率や、特定のディレクトリ(例: ./src/components/)のカバレッジ率に対して閾値を設けることができます。

11. スナップショットテスト (Snapshot Testing)

スナップショットテストは、UIコンポーネントや複雑なデータ構造、シリアライズ可能な値を、初めて実行したときの「スナップショット」と比較するテスト手法です。2回目以降のテスト実行時に、現在の結果と保存されたスナップショットが一致するかを検証します。一致しない場合は、コードに変更があったことを知らせてくれます。

主に以下の用途で使われます。

  • ReactなどのUIコンポーネントのレンダリング結果(DOM構造など)が意図せず変わっていないかの確認。
  • APIレスポンスや、ある処理によって生成される複雑なオブジェクトの構造が変わっていないかの確認。

スナップショットテストの例

sum.js の例を拡張して、オブジェクトを返す関数をテストする場合を考えます。

javascript
// processData.js
function processData(input) {
return {
original: input,
processed: input.toUpperCase(),
timestamp: new Date() // これはテストごとに変わるため、スナップショットには不向き
};
}
module.exports = processData;

この関数をスナップショットテストする場合、timestampのようにテスト実行ごとに値が変わる部分は除外するか、モックする必要があります。Jestはスナップショットから一部を除外する設定は直接提供していませんが、テスト対象のデータを加工してスナップショットと比較することは可能です。より一般的なUIコンポーネントのテストでは、JestとReact Testing Libraryなどを組み合わせ、レンダーされたDOM構造をスナップショットテストすることが多いです。

ここでは、シンプルな文字列の例でスナップショットテストを見てみましょう。

javascript
// formatString.js
function formatString(input) {
return `Formatted: ${input.trim()}`;
}
module.exports = formatString;

“`javascript
// formatString.test.js
const formatString = require(‘./formatString’);

test(‘should format the string correctly’, () => {
const input = ‘ hello world ‘;
const result = formatString(input);

// toMatchSnapshot() マッチャーを使用
expect(result).toMatchSnapshot();
});
“`

初めてこのテストを実行すると、formatString.test.jsと同じディレクトリに__snapshots__ディレクトリが作成され、その中に.snapという拡張子のファイルが生成されます。このファイルには、expect(result)に渡された値のシリアライズされた表現(スナップショット)が保存されます。

“`javascript
// snapshots/formatString.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[should format the string correctly 1] = "Formatted: hello world";
“`

2回目以降のテスト実行では、expect(result)の現在の値と、保存されているスナップショットが比較されます。

  • 一致した場合: テストは成功します。
  • 一致しない場合: テストは失敗し、現在の結果とスナップショットの差分が表示されます。これは、コードに変更があり、結果が変わったことを意味します。

スナップショットの更新

コードの変更が意図したものであり、スナップショットを新しい結果で更新したい場合は、以下のコマンドを実行します。

“`bash
npm test — -u

yarn test -u

“`

これにより、差分があった全てのスナップショットが最新の結果で更新されます。

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

  • 変更の検出: スナップショットテストは、コードの変更によって出力が変化したことを検出しますが、その変化が正しいか誤りかを判断するわけではありません。変更が発生した場合、その差分を注意深く確認し、期待通りの変化であればスナップショットを更新し、意図しない変化であればコードを修正する必要があります。
  • スナップショットのレビュー: プルリクエストなどでコード変更をレビューする際は、変更されたスナップショットファイルも必ずレビューしましょう。
  • 不安定な値: テスト実行ごとに変わりうる値(タイムスタンプ、ランダムなIDなど)を含むオブジェクトに対して安易にスナップショットテストを行うと、常にテストが失敗することになります。これらの値はモックするか、スナップショットに含めないように加工する必要があります。

スナップショットテストは、特にUIの見た目やデータ構造の不意な変更を防ぐのに非常に強力なツールですが、単体テストや結合テストと組み合わせて使用することが重要です。

12. その他の便利な機能と設定

Jestには、他にも開発効率を高めるための便利な機能や設定が多数あります。

  • 設定ファイル (jest.config.js): package.jsonにテスト設定を書く代わりに、独立したjest.config.jsファイルに設定を記述できます。大規模な設定や、コメントを含めたい場合に便利です。
  • モジュールパスエイリアス: WebpackやParcelなどで設定しているパスエイリアス(例: @/utilssrc/utilsを指すなど)をJestにも認識させる設定が可能です。moduleNameMapperオプションを使用します。
  • グローバル変数: 特定の関数やオブジェクトを全てのテストファイルでグローバルに利用可能にする設定です(非推奨)。
  • 環境 (Environments): JestはデフォルトでNode.js環境でテストを実行しますが、ブラウザ環境をシミュレートするためのjsdom環境も提供しています。フロントエンドのコンポーネントテストなどで利用します。testEnvironmentオプションで指定します。
  • トランスフォーマー (Transformers): TypeScriptやJSXなど、標準JavaScriptではないコードをテストする場合に、Jestが理解できるJavaScriptに変換するための設定です(例: ts-jest, babel-jest)。

これらの設定は、プロジェクトの要件に応じてpackage.jsonjestフィールド、またはjest.config.jsファイルに記述します。

13. テストを書く上でのベストプラクティス

Jestの使い方を理解したところで、効果的な単体テストを書くための一般的なベストプラクティスをいくつか紹介します。

  • テストの粒度: 単体テストはプログラムの最小単位をテストすることを目指します。大きすぎる単位をテストしようとすると、それは単体テストではなく結合テストに近くなり、テストが遅く、バグの原因特定が難しくなる傾向があります。
  • テストの独立性: 各テストケースは他のテストケースから独立して実行できるように記述します。テストの実行順序に依存したり、前のテストの副作用に影響されたりしないようにします。これはbeforeEach/afterEachやモックを適切に使うことで実現できます。
  • テストの命名: テスト名(test()describe()の第一引数)は、そのテストが何を確認しているのかを明確に記述します。例えば、「ユーザーがログインしている場合に、プロフィールページが表示されること」のように、仕様や振る舞いを記述すると良いでしょう。
  • 単一の検証 (Arrange-Act-Assert): 一つのテストケースでは、通常、一つの特定の振る舞いや結果を検証します。これはしばしば Arrange(準備)-Act(実行)-Assert(検証)のパターンに従います。
    • Arrange: テストに必要なデータ、状態、依存関係のモックなどを準備します。
    • Act: テスト対象のコード(関数呼び出し、メソッド呼び出しなど)を実行します。
    • Assert: 実行結果が期待通りであることをアサーション(expectとマッチャー)で検証します。
  • 実装詳細ではなく振る舞いをテストする: テストは、コードの内部実装ではなく、そのコードが外部からどのように見えるか、どのような入力に対してどのような出力を返すか、どのような副作用を持つかといった振る舞いをテストするべきです。実装詳細に依存するテストは、リファクタリング時に簡単に壊れてしまい、テストが保守しにくくなります。
  • エッジケースとエラーハンドリングをテストする: 通常の成功パスだけでなく、無効な入力、境界値、エラーが発生した場合などのエッジケースやエラーハンドリングのロジックもテストすることが重要です。
  • テストコードも保守が必要: テストコードはプロダクションコードと同様に、可読性が高く、構造化され、保守しやすいように書く必要があります。リファクタリングが必要になることもあります。

これらのベストプラクティスを意識することで、テストはより効果的で、コードの品質向上に本当に貢献するものとなります。

14. まとめ

本記事では、JavaScriptのテスティングフレームワークJestに焦点を当て、単体テストの基本から応用までを網羅的に解説しました。

  • ソフトウェアテスト、特に単体テストがなぜ重要なのかを理解しました。
  • Jestの魅力的な特徴(オールインワン、シンプル設定、高速性など)を知りました。
  • Jestの環境構築から、最初のテストコード実行までの手順を確認しました。
  • describe, test/it, expectといったJestのコアな要素と、様々なマッチャーの使い方を学びました。
  • 非同期コード(Promise, async/await)を正しくテストする方法を習得しました。
  • 単体テストにおいて不可欠なモックの概念と、jest.fn(), jest.mock(), jest.spyOn()の使い方を理解しました。
  • before/afterフックを使ったテストのセットアップとティアダウン、テストファイルの組織化について学びました。
  • コマンドラインオプションやウォッチャーを使った効率的なテスト実行方法を確認しました。
  • コードカバレッジレポートの生成方法とその解釈、限界について理解しました。
  • UIや複雑な構造の変化を検出するスナップショットテストについて触れました。
  • 効果的なテストを書くためのベストプラクティスをいくつか紹介しました。

Jestは非常に強力で柔軟なツールであり、JavaScriptプロジェクトの品質と開発効率を向上させるための強力な味方となります。単体テストは最初こそ少し手間に感じるかもしれませんが、一度習慣化すれば、開発プロセスの自然な一部となり、将来的な大きなメリットを実感できるはずです。

この記事が、あなたがJestを使ったJavaScript単体テストの世界に足を踏み入れるための一助となれば幸いです。実際にコードを書きながら、様々な機能を試してみてください。

Happy Testing!

コメントする

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

上部へスクロール