現代のWeb開発、特にJavaScriptを用いた開発において、コードの品質と信頼性を担保することは非常に重要です。機能追加や仕様変更、リファクタリングを迅速かつ安全に進めるためには、堅牢なテストスイートが不可欠となります。本記事では、JavaScriptのテストフレームワークとしてデファクトスタンダードの地位を確立している「Jest」について、その概要から基本的な使い方、応用的な機能までを網羅的に解説します。
Jestをこれから学びたいと考えている初心者の方から、さらに理解を深めたい中級者の方まで、幅広く役立つ情報を提供します。この記事を読めば、Jestの導入から設定、テストコードの記述、モックや非同期処理といった高度なテスト手法まで、一通りの知識を習得できるでしょう。
目次
Jestとは
まずはじめに、Jestがどのようなツールであり、なぜ多くの開発者に選ばれているのか、その基本的な概念とメリットを詳しく見ていきましょう。
JavaScriptのテスト用フレームワーク
Jestは、Meta社(旧Facebook)によって開発・メンテナンスされている、オープンソースのJavaScriptテストフレームワークです。もともとはReactアプリケーションのテストを効率的に行うために開発されましたが、現在ではその汎用性の高さから、Reactに限らずVue.js、Angularといった主要なフロントエンドフレームワークはもちろん、Node.jsを用いたバックエンド開発、TypeScriptプロジェクトなど、あらゆるJavaScript環境で標準的に利用されています。
ソフトウェア開発における「テスト」とは、作成したプログラムが意図した通りに正しく動作するかを確認する工程を指します。手動でブラウザを操作したり、コンソールに結果を出力して目視で確認したりする方法もありますが、プロジェクトが大規模化・複雑化するにつれて、この手動テストには限界が生じます。
- 時間とコストの増大: 機能が増えるたびに、確認すべき項目が指数関数的に増加します。
- ヒューマンエラーの発生: 確認漏れや見間違いなど、人為的なミスが発生しやすくなります。
- 再現性の欠如: 同じ手順を毎回正確に繰り返すことが困難です。
こうした問題を解決するのが「自動テスト」です。テストコードを記述することで、これらの確認作業をプログラムに自動的に実行させます。Jestは、この自動テストを効率的かつ快適に記述・実行するための環境を提供してくれるツールなのです。
Jestが提供するテストの主な種類には、以下のようなものがあります。
- 単体テスト(Unit Test): 関数やコンポーネントといった、プログラムの最小単位が個別に正しく動作するかを検証します。
- 結合テスト(Integration Test): 複数のモジュールを組み合わせた際に、それらが連携して正しく機能するかを検証します。
- E2Eテスト(End-to-End Test): ユーザーの操作を模倣し、アプリケーション全体の流れが正常に動作するかを検証します。(Jest単体でも可能ですが、CypressやPlaywrightといった専用ツールと組み合わせて使われることが多いです)
Jestは特に単体テストや結合テストの領域でその真価を発揮し、開発者が自信を持ってコードをリリースするための強力な支えとなります。
Jestを利用するメリット
数あるJavaScriptテストフレームワークの中で、なぜJestがこれほどまでに広く支持されているのでしょうか。その理由は、Jestが持つ数々の優れた特徴にあります。
ゼロコンフィグで簡単に始められる
Jestの最大のメリットの一つは、その「ゼロコンフィグ(Zero Configuration)」思想です。多くのテストツールでは、テストを実行するためにBabelやwebpackといったトランスパイラやバンドラ、アサーションライブラリ、モックライブラリなどを個別にインストールし、それらを連携させるための複雑な設定ファイルを記述する必要がありました。
しかし、Jestはインストールするだけで、ほとんどのJavaScriptプロジェクトで即座にテストを開始できます。基本的な設定が内部に組み込まれているため、開発者は環境構築の煩わしさから解放され、本来の目的であるテストコードの記述に集中できます。この導入の手軽さは、特にテストに不慣れな初心者や、迅速に開発を始めたいプロジェクトにとって大きな魅力です。
オールインワンで機能が豊富
Jestは単なるテストランナー(テストを実行するツール)ではありません。テストを記述・実行するために必要な機能がほぼすべて同梱された「オールインワン」のフレームワークです。
- テストランナー: テストファイルを探索し、効率的に実行します。
- アサーションライブラリ:
expect
といった構文と、後述する豊富な「マッチャー」を提供し、テストの期待結果を検証します。 - モック機能: 外部APIやデータベース、他のモジュールへの依存を切り離すための強力なモック(偽物)機能が組み込まれています。
- カバレッジレポート: コードのどの部分がテストによってカバーされているかを示すカバレッジレポートを簡単に生成できます。
従来、これらの機能を実現するためには、Mocha(テストランナー)、Chai(アサーション)、Sinon(モック)といった複数のライブラリを組み合わせて使うのが一般的でした。Jestはこれらすべてを一つのパッケージで提供するため、ライブラリ間のバージョンの互換性を気にする必要がなく、学習コストも低く抑えられます。
テストの実行が高速
開発サイクルにおいて、テストの実行時間は生産性に直結します。テストの実行が遅いと、コードを少し変更するたびに長い待ち時間が発生し、開発者の集中力を削いでしまいます。
Jestはテストの高速化のために様々な工夫を凝らしています。最も特徴的なのは、テストスイート(テストファイルの集まり)をワーカープロセスに分割し、並列で実行する仕組みです。これにより、マルチコアCPUの性能を最大限に活かし、テスト全体の所要時間を大幅に短縮します。
さらに、「Watchモード」という強力な機能も備わっています。このモードを有効にすると、Jestはファイルの変更を監視し、変更が加えられたファイルや、その変更に影響を受けるファイルに関連するテストのみを自動で再実行します。すべてのテストを毎回実行する必要がないため、フィードバックループが非常に高速になり、リズミカルな開発体験を実現します。
分かりやすいAPIと豊富なマッチャー
JestのAPIは、非常に直感的で人間が読みやすいように設計されています。テストコードは、単にプログラムの正しさを検証するだけでなく、そのプログラムが「どのように振る舞うべきか」を示すドキュメントとしての役割も担います。
Jestでは、describe
, test(it)
, expect
といったキーワードを使って、自然言語に近い形でテストの構造と意図を記述できます。
describe('ユーザー認証機能', () => {
it('正しいパスワードでログインできるべき', () => {
// ... テストコード
});
it('間違ったパスワードではエラーを返すべき', () => {
// ... テストコード
});
});
このように記述されたテストは、プログラマーでなくともその目的を理解しやすいでしょう。
また、テストの検証部分で使われる「マッチャー」が非常に豊富です。.toBe()
(厳密な等価性)、.toEqual()
(オブジェクトの値の等価性)、.toBeTruthy()
(真偽値の検証)、.toContain()
(配列の要素の存在確認)など、様々なシチュエーションに対応するマッチャーが用意されており、テストの意図を明確かつ簡潔に表現できます。
スナップショットテスト機能を標準搭載
スナップショットテストは、Jestが持つユニークで強力な機能の一つです。これは、特にReactなどのUIコンポーネントや、巨大なJSONオブジェクトの構造をテストする際に絶大な効果を発揮します。
スナップショットテストでは、最初のテスト実行時に、対象となるコンポーネントのレンダリング結果やオブジェクトの構造を「スナップショット」としてファイルに保存します。2回目以降のテストでは、現在の結果と保存されたスナップショットを比較し、もし差分があればテストは失敗します。
これにより、「意図しない変更」を確実に検知できます。例えば、CSSの調整で見た目を少し変えたつもりが、誤ってコンポーネントのDOM構造まで破壊してしまった、といったミスを防ぐことができます。変更が意図したものであれば、簡単なコマンドでスナップショットを更新するだけです。この機能により、UIのデグレード(意図しない品質低下)に対する強力なセーフティネットを構築できます。
Jestの導入と環境設定
Jestのメリットを理解したところで、次はいよいよ実際にJestをプロジェクトに導入し、テストを実行するための環境を整えていきましょう。ここでは、Node.jsとnpm(Node Package Manager)が既にインストールされていることを前提として進めます。
プロジェクトの準備とJestのインストール
まずは、Jestを導入するためのプロジェクトディレクトリを作成します。
# プロジェクト用のディレクトリを作成し、移動する
mkdir jest-practice
cd jest-practice
# package.jsonファイルを生成する
npm init -y
npm init -y
コマンドを実行すると、プロジェクトの情報を管理するための package.json
ファイルがカレントディレクトリに生成されます。-y
フラグは、すべての質問にデフォルト値で「はい」と答えるオプションです。
次に、Jestをプロジェクトにインストールします。Jestは開発時にのみ使用するツールであり、ビルドされた本番環境のアプリケーションには含まれる必要がないため、--save-dev
(または -D
)オプションを付けて「開発時依存(devDependencies)」としてインストールするのが一般的です。
npm install --save-dev jest
このコマンドを実行すると、node_modules
ディレクトリと package-lock.json
ファイルが作成され、package.json
の devDependencies
セクションに jest
が追記されます。これで、プロジェクト内でJestを利用する準備が整いました。
package.jsonにテストコマンドを登録する
Jestをインストールしただけでは、jest
コマンドはプロジェクトのローカルにインストールされているため、ターミナルから直接 jest
と打っても実行できません(npx jest
のように npx
を使えば実行できます)。
より便利にテストを実行するために、package.json
の scripts
オブジェクトにテスト用のコマンドを登録するのが定石です。
package.json
ファイルを開き、"scripts"
の部分を以下のように編集します。
{
"name": "jest-practice",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.7.0" // バージョンは異なる場合があります
}
}
このように "test": "jest"
という一行を追加することで、npm test
というコマンドを実行した際に、内部的に jest
コマンドが呼び出されるようになります。これはnpmの標準的な使い方であり、プロジェクトの他のメンバーも迷うことなくテストを実行できます。
さらに、便利なオプションを付けたコマンドを登録しておくことも可能です。
- Watchモード:
jest --watchAll
- ファイルの変更を監視し、全てのテストを自動で再実行します。
- カバレッジレポートの生成:
jest --coverage
- テストカバレッジを計測し、レポートを生成します。
これらのコマンドも scripts
に登録しておくと便利です。
"scripts": {
"test": "jest",
"test:watch": "jest --watchAll",
"test:coverage": "jest --coverage"
}
こうすることで、npm run test:watch
や npm run test:coverage
といったコマンドで、各機能を簡単に利用できるようになります。
設定ファイル(jest.config.js)を作成する
前述の通り、Jestはゼロコンフィグで動作しますが、プロジェクトの要件に合わせてより詳細な設定を行いたい場合もあります。その際は、プロジェクトのルートディレクトリに設定ファイルを作成します。ファイル名は jest.config.js
とするのが一般的です。
手動でファイルを作成して一から記述することもできますが、Jestには対話形式で設定ファイルを自動生成してくれる便利なコマンドが用意されています。
npx jest --init
このコマンドを実行すると、ターミナル上でいくつかの質問が表示されます。これらの質問に答えていくことで、プロジェクトに適した jest.config.js
が生成されます。
以下は、質問の例と選択肢です。
- Choose the test environment that will be used for testing (テスト環境を選択してください)
node
: Node.js環境(バックエンドなど)jsdom
: ブラウザに似た環境(Reactなどフロントエンド)- Do you want Jest to add coverage reports? (カバレッジレポートを生成しますか?)
yes
/no
- Which provider should be used to instrument code for coverage? (カバレッジ計測にどのプロバイダを使いますか?)
v8
: Node.jsに組み込まれた高速なカバレッジプロバイダbabel
: Babelを利用している場合- Automatically clear mock calls, instances, contexts and results before every test? (各テストの前に自動でモックをクリアしますか?)
yes
/no
(通常はyes
が推奨されます)
すべての質問に答え終えると、以下のような内容の jest.config.js
ファイルが生成されます。(選択によって内容は異なります)
// jest.config.js
module.exports = {
// 各テストの前にモックを自動的にクリアするかどうか
clearMocks: true,
// テストカバレッジ情報を収集するかどうか
collectCoverage: true,
// カバレッジレポートが出力されるディレクトリ
coverageDirectory: "coverage",
// カバレッジ情報の収集に使用するプロバイダ
coverageProvider: "v8",
// テスト環境
testEnvironment: "node",
// ...他にも多くのオプションがあります
};
生成されたファイルはJavaScriptのモジュールなので、コメントを加えたり、条件に応じて動的に設定を変更したりすることも可能です。testMatch
でテストファイルのパターンを指定したり、moduleNameMapper
でモジュールのパス解決のエイリアスを設定したりと、様々なカスタマイズが可能です。ゼロコンフィグで始めて、必要になったら jest.config.js
を作成して拡張していく、という進め方がおすすめです。
参照:Jest公式サイト
Jestの基本的なテストの書き方と実行方法
環境設定が完了したら、いよいよテストコードを書いていきましょう。ここでは、シンプルな関数を題材に、Jestの基本的なテストコードの構造と実行方法を学びます。
テスト対象のサンプルコード
まずは、テストの対象となる簡単な関数を用意します。プロジェクトのルートに sum.js
というファイルを作成し、以下のコードを記述してください。この関数は、二つの数値を引数に取り、その合計を返すものです。
// sum.js
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('引数は両方とも数値でなければなりません。');
}
return a + b;
}
module.exports = sum;
このコードでは、sum
関数を定義し、Node.jsのモジュールシステムである module.exports
を使って、他のファイルからこの関数を require
できるようにエクスポートしています。また、引数が数値でない場合にはエラーをスローする、という簡単なバリデーションも加えています。
テストファイルの命名規則
Jestは、特定の命名規則に従ったファイルを自動的にテストファイルとして認識します。主な規則は以下の2つです。
.test.js
または.spec.js
という接尾辞をつける- 例:
sum.test.js
,sum.spec.js
spec
は「仕様(specification)」の略です。どちらを使っても機能的な違いはありませんが、プロジェクト内で統一するのが一般的です。
- 例:
__tests__
という名前のディレクトリにファイルを作成する- 例:
__tests__/sum.js
- アンダースコアが前後に2つずつ付く点に注意してください。
- 例:
最も広く使われているのは、テスト対象のファイルと同じ階層に .test.js
ファイルを配置する方法です(例: sum.js
と sum.test.js
)。これにより、テスト対象のコードとテストコードが近くに配置され、関連性が分かりやすくなります。
今回はこの慣習に従い、sum.js
と同じディレクトリに sum.test.js
というファイルを作成しましょう。
describe, test(it)を使ったテストの構造
それでは sum.test.js
の中に、sum
関数をテストするためのコードを書いていきます。Jestのテストは、主に describe
、test
(またはit
)、expect
の3つの主要な関数で構成されます。
describe(name, fn)
: 関連するテストケースをグループ化するためのブロックです。「テストスイート」とも呼ばれます。第一引数にはテストグループの説明を文字列で、第二引数には実際のテストコードを含む関数を渡します。test(name, fn)
: 個々のテストケースを定義するブロックです。describe
の中に複数配置することができます。第一引数にはそのテストが何を検証するものなのかを説明する文字列を、第二引数にはテストロジックを含む関数を渡します。it
はtest
のエイリアス(別名)であり、全く同じ機能です。BDD(ビヘイビア駆動開発)の文脈で「It should …(それは〜であるべきだ)」と英語で自然に読めることからit
が好まれることもあります。expect(value)
: アサーション(検証)を開始するための関数です。引数にはテスト対象の関数を実行した結果など、検証したい値を渡します。これ自体は何もせず、後述する「マッチャー」と組み合わせて使用します。
これらの関数を使って sum.test.js
を記述すると、以下のようになります。
// sum.test.js
// テスト対象の関数をインポートする
const sum = require('./sum');
// 'sum' 関数に関するテストスイートを開始
describe('sum関数のテスト', () => {
// 個別のテストケース1
test('1 + 2 を計算すると 3 になること', () => {
// expect(検証したい値).マッチャー(期待する値)
expect(sum(1, 2)).toBe(3);
});
// 個別のテストケース2
test('負の数の足し算が正しく行われること', () => {
expect(sum(-1, -5)).toBe(-6);
});
// 個別のテストケース3(itを使用)
it('数値でない引数が渡された場合にエラーをスローすること', () => {
// エラーがスローされることをテストする
// toThrowマッチャーには、期待するエラーメッセージを渡すこともできる
expect(() => sum(1, 'a')).toThrow('引数は両方とも数値でなければなりません。');
});
});
このコードのポイントは以下の通りです。
- まず
require('./sum')
でテストしたいsum
関数を読み込みます。 describe('sum関数のテスト', ...)
で、sum
関数に関連するテストをまとめます。test(...)
やit(...)
で、具体的なテストシナリオを記述します。- 「1 + 2」のような基本的なケース
- 「-1 + -5」のようなエッジケース
- 「数値でない引数」のような異常系ケース
expect(sum(1, 2)).toBe(3)
の部分がテストの核となるアサーションです。「sum(1, 2)
の実行結果が3
であること(toBe
)を期待する(expect
)」と読むことができます。- エラーをテストする場合は、
expect
の引数にテスト対象の処理をアロー関数でラップして渡します。これは、sum(1, 'a')
を直接実行するとその場でエラーが発生してテストが停止してしまうためです。
テストを実行するコマンド
テストコードが書けたら、いよいよ実行してみましょう。package.json
にコマンドを登録したので、ターミナルで以下のコマンドを実行します。
npm test
コマンドが成功すると、ターミナルに以下のような結果が出力されます。
PASS ./sum.test.js
sum関数のテスト
✓ 1 + 2 を計算すると 3 になること (2ms)
✓ 負の数の足し算が正しく行われること
✓ 数値でない引数が渡された場合にエラーをスローすること (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.41 s
Ran all test suites.
PASS
と緑色で表示され、各テストケースにチェックマーク(✓)がついていれば、すべてのテストが成功したことを意味します。
では、もしテストが失敗した場合はどうなるでしょうか。試しに sum.js
の実装をわざと間違えてみましょう。
// sum.js
function sum(a, b) {
// わざと間違える
return a - b;
}
module.exports = sum;
この状態で再度 npm test
を実行すると、今度は以下のような失敗のレポートが出力されます。
FAIL ./sum.test.js
sum関数のテスト
✕ 1 + 2 を計算すると 3 になること (3ms)
✓ 負の数の足し算が正しく行われること
✓ 数値でない引数が渡された場合にエラーをスローすること
● sum関数のテスト › 1 + 2 を計算すると 3 になること
expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: -1
4 | test('1 + 2 を計算すると 3 になること', () => {
5 | // expect(検証したい値).マッチャー(期待する値)
> 6 | expect(sum(1, 2)).toBe(3);
| ^
7 | });
8 |
at Object.<anonymous> (sum.test.js:6:23)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 2 passed, 3 total
Snapshots: 0 total
Time: 0.45 s
FAIL
と赤色で表示され、どのテストケースが失敗したのか(✕
マーク)、期待していた値(Expected: 3)と実際に得られた値(Received: -1)がどのように違ったのかが明確に示されます。この詳細なレポートにより、開発者は問題の箇所を迅速に特定し、修正作業に取り掛かることができます。
テストの成否を判定するマッチャーの基本
Jestのテストコードの中核をなすのが「マッチャー(Matcher)」です。expect
でラップした値が、期待通りであるかを様々な角度から検証する役割を担います。ここでは、Jestが提供する多種多様なマッチャーの中から、特によく使われるものを厳選して紹介します。
マッチャーとは
マッチャーは、expect(value).matcherName(expectedValue)
の形式で使用される関数の総称です。expect()
に渡された value
(実際の値)を、matcherName()
の引数に渡された expectedValue
(期待値)と比較し、テストの成功(pass)または失敗(fail)を決定します。
例えば、expect(2 + 2).toBe(4)
というコードでは、toBe
がマッチャーにあたります。これは 2 + 2
の結果である 4
が、期待値である 4
と「厳密に等しい」ことを検証します。
適切なマッチャーを選択することで、テストの意図が明確になり、可読性が高く、信頼性のあるテストコードを記述できます。
マッチャーのカテゴリ | 主なマッチャー | 用途 |
---|---|---|
等価性 | toBe , toEqual |
値が等しいかどうかをチェックする |
Truthiness | toBeTruthy , toBeFalsy |
値が真偽値としてどう評価されるかをチェックする |
Null/Undefined | toBeNull , toBeUndefined , toBeDefined |
null や undefined を専門にチェックする |
数値比較 | toBeGreaterThan , toBeLessThan , toBeCloseTo |
数値の大小や近似値をチェックする |
文字列 | toMatch |
文字列が正規表現にマッチするかをチェックする |
配列・イテラブル | toContain , toHaveLength |
配列に要素が含まれるか、長さをチェックする |
例外 | toThrow |
関数がエラーをスローするかをチェックする |
否定 | .not |
他のマッチャーと組み合わせて条件を反転させる |
よく使われるマッチャーの例
それでは、具体的なマッチャーの使い方をコード例と共に見ていきましょう。
等価性をチェックする (toBe, toEqual)
.toBe(value)
Object.is
を使用して、厳密な等価性(===
とほぼ同じ)をチェックします。プリミティブ型(number
,string
,boolean
,null
,undefined
,symbol
,bigint
)の比較に使用します。オブジェクトや配列の場合、参照(メモリ上のアドレス)が同じでない限り失敗します。
javascript
test('toBeマッチャーの例', () => {
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');
// const obj1 = { a: 1 };
// const obj2 = { a: 1 };
// expect(obj1).toBe(obj2); // これは失敗する(参照が異なるため)
});
.toEqual(value)
オブジェクトや配列の値を再帰的に比較します。プロパティや要素がすべて同じであれば、参照が異なっていてもテストは成功します。オブジェクトや配列の内容を比較したい場合は、toBe
ではなくtoEqual
を使用します。
“`javascript
test(‘toEqualマッチャーの例’, () => {
const data = { one: 1 };
data[‘two’] = 2;
expect(data).toEqual({ one: 1, two: 2 });
const arr = ['a', 'b'];
expect(arr).toEqual(['a', 'b']);
});
``
toBe
**と
toEqual` の使い分けは、Jestの基本であり非常に重要です。**
Truthinessをチェックする (toBeTruthy, toBeFalsy)
JavaScriptには true
/ false
以外にも「truthy(真値)」や「falsy(偽値)」と評価される値があります。これらをチェックするためのマッチャーです。
.toBeTruthy()
:if
文の条件式でtrue
と評価される値をチェックします。true
だけでなく、1
や'hello'
、{}
、[]
などもtrue
となります。.toBeFalsy()
:if
文の条件式でfalse
と評価される値をチェックします。false
だけでなく、0
、''
(空文字)、null
、undefined
、NaN
などもfalse
となります。
test('truthinessマッチャーの例', () => {
expect('some string').toBeTruthy();
expect(1).toBeTruthy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
});
nullやundefinedをチェックする (toBeNull, toBeUndefined, toBeDefined)
null
や undefined
は toBeFalsy
でもチェックできますが、より意図を明確にするために専用のマッチャーが用意されています。
.toBeNull()
: 値がnull
であることのみをチェックします。.toBeUndefined()
: 値がundefined
であることのみをチェックします。.toBeDefined()
: 値がundefined
でないことをチェックします。toBeUndefined
の逆です。
test('null/undefinedマッチャーの例', () => {
const n = null;
let u; // undefined
const d = 'defined';
expect(n).toBeNull();
expect(u).toBeUndefined();
expect(d).toBeDefined();
// expect(n).toBeUndefined(); // これは失敗する
});
数値を比較する (toBeGreaterThan, toBeLessThan)
数値の大小を比較するためのマッチャーです。
.toBeGreaterThan(number)
:>
(より大きい)をチェックします。.toBeGreaterThanOrEqual(number)
:>=
(以上)をチェックします。.toBeLessThan(number)
:<
(より小さい)をチェックします。.toBeLessThanOrEqual(number)
:<=
(以下)をチェックします。
test('数値比較マッチャーの例', () => {
const value = 10;
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThanOrEqual(10);
});
また、浮動小数点数の計算では、誤差が生じることがあります。0.1 + 0.2
が 0.30000000000000004
になるのは有名な話です。このような場合に .toBe()
を使うと失敗するため、.toBeCloseTo()
を使います。
test('浮動小数点数の比較', () => {
const value = 0.1 + 0.2;
// expect(value).toBe(0.3); // これは失敗する
expect(value).toBeCloseTo(0.3); // これは成功する
});
文字列をチェックする (toMatch)
文字列が特定のパターンに一致するかを正規表現でチェックします。
.toMatch(regexpOrString)
: 引数に正規表現オブジェクトまたは文字列を渡します。
test('文字列マッチャーの例', () => {
expect('team').not.toMatch(/I/); // 'I'という文字は含まれない
expect('Christoph').toMatch(/stop/); // 'stop'という文字列が含まれる
});
配列やイテラブルをチェックする (toContain)
配列やイテラブル(Set
など)に特定の要素が含まれているかをチェックします。
.toContain(item)
: 配列内にitem
が存在すれば成功します。
test('配列マッチャーの例', () => {
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
例外の発生をチェックする (toThrow)
関数を実行した際に、意図通りにエラー(例外)がスローされるかを検証します。APIの不正な入力など、異常系のテストで頻繁に使用されます。
.toThrow(error?)
: 引数を省略すると、何らかのエラーがスローされれば成功します。引数に文字列や正規表現、Errorクラスを指定することで、特定のエラーメッセージやエラー型であるかをより厳密にチェックできます。
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('例外マッチャーの例', () => {
// expectに関数を直接渡すのではなく、アロー関数でラップする
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// エラーメッセージの完全一致または部分一致をチェック
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/wrong JDK/);
});
条件を否定する (.not)
.not
は、他のマッチャーと組み合わせて、その結果を反転させます。「〜でないこと」をテストしたい場合に使用します。
test('.not修飾子の例', () => {
expect(2 + 2).not.toBe(5); // 5ではないこと
expect([1, 2, 3]).not.toContain(4); // 4を含まないこと
});
これらのマッチャーを使いこなすことが、表現力豊かで堅牢なテストを書くための第一歩です。公式ドキュメントにはさらに多くのマッチャーが記載されているため、必要に応じて参照することをおすすめします。
参照:Jest公式サイト Expect API
非同期処理のテスト方法
現代のJavaScriptアプリケーションでは、APIからのデータ取得、ファイルの読み書き、タイマー処理など、非同期処理が多用されます。これらの処理は、完了するまでに時間がかかるため、通常の同期的なテスト手法では正しく検証できません。Jestは、非同期処理をテストするためのシンプルで強力な仕組みを提供しています。
なぜ特別な仕組みが必要なのでしょうか? それは、Jestのテスト関数は、コードの実行が完了した時点で終了してしまうからです。非同期処理の完了を待たずにテストが終了してしまうと、アサーションが実行される前にテストが「成功」したと見なされたり、意図しない結果になったりします。これを避けるため、Jestに非同期処理の完了を明示的に伝える必要があります。
コールバック関数をテストする
古くからある非同期処理のパターンとして、処理の完了時に呼び出される「コールバック関数」を引数に取るスタイルがあります。
この形式の非同期関数をテストする場合、テスト関数(test
やit
)の引数に done
という名前の引数を追加します。Jestは、テスト関数に done
引数が存在することを確認すると、done()
が呼び出されるまでテストの完了を待機します。
// テスト対象の非同期関数(データを取得してコールバックを呼ぶ)
function fetchData(callback) {
setTimeout(() => {
callback('peanut butter');
}, 100);
}
// コールバックを使うテスト
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done(); // テストが完了したことをJestに伝える
} catch (error) {
done(error); // エラーが発生した場合もdoneを呼び、テストを失敗させる
}
}
fetchData(callback);
});
このテストのポイントは以下の通りです。
- テスト関数の引数に
done
を追加します。 - 非同期処理が完了し、アサーションを実行した後に、必ず
done()
を呼び出します。 expect
が失敗するとエラーがスローされ、done()
が呼ばれずにテストがタイムアウトしてしまいます。これを防ぐため、try...catch
ブロックでアサーションを囲み、catch
ブロックでもdone(error)
を呼び出してテストを即座に失敗させるのが定石です。
done()
を呼び忘れると、Jestはテストがいつ終わるかわからず、設定されたタイムアウト時間(デフォルトは5秒)が経過した後にテストを失敗させます。
Promiseをテストする
現在では、コールバックよりもPromiseを用いた非同期処理が主流です。Promiseを返す関数をテストするのは、コールバックよりもずっと簡単です。
テスト関数からPromiseを return
するだけで、JestはそのPromiseが解決(resolve)または拒否(reject)されるまで待機します。
// Promiseを返す非同期関数
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('peanut butter'), 100);
});
}
function fetchDataPromiseReject() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('error')), 100);
});
}
// Promiseをテストする (resolveされるケース)
test('the data is peanut butter', () => {
// Promiseをreturnするのを忘れないこと!
return fetchDataPromise().then(data => {
expect(data).toBe('peanut butter');
});
});
// Promiseをテストする (rejectされるケース)
test('the fetch fails with an error', () => {
// アサーションの数を期待する
expect.assertions(1);
return fetchDataPromiseReject().catch(e => {
expect(e).toMatchObject({ message: 'error' });
});
});
Promiseを return
し忘れると、fetchDataPromise()
が完了する前にテストが終了してしまうため、注意が必要です。
さらに、JestにはPromise専用の便利なマッチャー .resolves
と .rejects
があります。これらを使うと、より簡潔に記述できます。
// .resolves を使うテスト
test('the data is peanut butter with .resolves', () => {
return expect(fetchDataPromise()).resolves.toBe('peanut butter');
});
// .rejects を使うテスト
test('the fetch fails with an error with .rejects', () => {
return expect(fetchDataPromiseReject()).rejects.toThrow('error');
});
return
キーワードはここでも必須です。
async/await構文をテストする
async/await
は、Promiseベースの非同期コードを、同期的であるかのように直感的に書けるようにする構文です。Jestでの非同期テストも、async/await
を使うのが最も推奨される現代的な方法です。
テスト関数を async
として宣言し、Promiseを返す関数の呼び出しの前に await
を付けるだけです。
// async/await を使ったテスト (resolveされるケース)
test('the data is peanut butter with async/await', async () => {
const data = await fetchDataPromise();
expect(data).toBe('peanut butter');
});
// async/await を使ったテスト (rejectされるケース)
test('the fetch fails with an error with async/await', async () => {
expect.assertions(1);
try {
await fetchDataPromiseReject();
} catch (e) {
expect(e).toMatchObject({ message: 'error' });
}
});
async/await
を使うと、return
や .then()
を書く必要がなくなり、コードが非常にシンプルで読みやすくなります。.resolves
や .rejects
マッチャーと組み合わせることも可能です。
// async/await と .resolves/.rejects を組み合わせる
test('the data is peanut butter with async/await and .resolves', async () => {
await expect(fetchDataPromise()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error with async/await and .rejects', async () => {
await expect(fetchDataPromiseReject()).rejects.toThrow('error');
});
expect.assertions(number)
は、テスト中に期待されるアサーション(expect
)の呼び出し回数を宣言するのに役立ちます。特に、Promiseが拒否されるケースのテストで重要です。もし fetchDataPromiseReject()
が誤ってresolveされてしまった場合、catch
ブロックは実行されず、アサーションが一度も実行されないままテストが成功してしまいます。expect.assertions(1)
を記述しておくことで、アサーションが1回も実行されなかった場合にテストを失敗させ、このようなバグを見つけやすくします。
テストのセットアップと後処理
実際のテストでは、個々のテストケースを実行する前に共通の準備(セットアップ)を行ったり、テストが終わった後に後片付け(後処理、ティアダウン)を行ったりする必要が頻繁に生じます。例えば、テスト用のデータベースを初期化したり、各テストで使うオブジェクトを生成したり、テスト後に作成したファイルを削除したりといった作業です。
Jestでは、これらの処理を効率的に行うための「フック(Hook)」関数が用意されています。それが beforeAll
, afterAll
, beforeEach
, afterEach
です。
各テストの前後に処理を実行する (beforeEach, afterEach)
beforeEach(fn)
:describe
ブロック内の各test
ケースが実行される直前に毎回呼び出されます。afterEach(fn)
:describe
ブロック内の各test
ケースが実行された直後に毎回呼び出されます。
これらは、テストケース同士が互いに影響を与えないように、テスト環境をクリーンな状態に保つために非常に重要です。各テストが独立していることを保証するのに役立ちます。
具体例:テストデータの初期化
都市のデータベースを操作するテストを考えてみましょう。各テストでデータベースが同じ初期状態から始まるように、beforeEach
でデータを初期化します。
let cityDatabase = [];
function initializeCityDatabase() {
cityDatabase = ['Vienna', 'San Juan', 'Kyoto'];
}
function clearCityDatabase() {
cityDatabase = [];
}
function isCity(name) {
return cityDatabase.includes(name);
}
// beforeEachとafterEachを使ったテスト
describe('city database tests', () => {
// 各テストの前に、データベースを初期化する
beforeEach(() => {
initializeCityDatabase();
});
// 各テストの後に、データベースをクリアする
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
test('city database does not have Rome', () => {
expect(isCity('Rome')).toBeFalsy();
});
});
この例では、3つの test
ケースが実行されるたびに、その直前で initializeCityDatabase()
が呼ばれ、cityDatabase
は ['Vienna', 'San Juan', 'Kyoto']
という状態に戻ります。そして、テストの直後には clearCityDatabase()
が呼ばれます。これにより、あるテストケース内で行われた操作(例えばデータベースに都市を追加するなど)が、次のテストケースに影響を及ぼすのを防げます。
全テストの最初と最後に処理を実行する (beforeAll, afterAll)
beforeAll(fn)
:describe
ブロック内のすべてのtest
ケースが実行される前に、一度だけ呼び出されます。afterAll(fn)
:describe
ブロック内のすべてのtest
ケースが実行された後に、一度だけ呼び出されます。
これらは、セットアップや後処理に時間がかかる場合に特に有効です。例えば、データベースへの接続や切断、テスト用サーバーの起動や停止といった処理は、テストケースごとに実行すると非常に非効率です。
具体例:データベース接続
let db;
function connectDatabase() {
// 実際にはここでDB接続処理を行う
console.log('Connecting to database...');
db = { status: 'connected' };
}
function disconnectDatabase() {
// 実際にはここでDB切断処理を行う
console.log('Disconnecting from database...');
db = null;
}
// beforeAllとafterAllを使ったテスト
describe('database connection', () => {
// 全テストの最初に一度だけ実行
beforeAll(() => {
return connectDatabase(); // 非同期処理の場合はPromiseを返すかasync/awaitを使う
});
// 全テストの最後に一度だけ実行
afterAll(() => {
return disconnectDatabase();
});
test('can access the database', () => {
expect(db.status).toBe('connected');
});
// ... 他のデータベース関連のテスト
});
この例では、beforeAll
で一度だけデータベースに接続し、すべてのテストが終わった後に afterAll
で接続を切断しています。これにより、テスト全体の実行時間を短縮できます。
describeブロックでのスコープ
これらのフック関数は、ファイルのトップレベルだけでなく、特定の describe
ブロック内に記述することもできます。ブロック内に記述されたフックは、その describe
ブロック内のテストにのみ適用されます。
// トップレベルのフック
beforeEach(() => console.log('Outer beforeEach'));
afterEach(() => console.log('Outer afterEach'));
describe('Scoped Hooks', () => {
// このdescribeブロック内でのみ有効なフック
beforeEach(() => console.log('Inner beforeEach'));
afterEach(() => console.log('Inner afterEach'));
test('test 1', () => console.log('test 1'));
});
上記のコードを実行すると、コンソールには以下の順で出力されます。
Outer beforeEach
Inner beforeEach
test 1
Inner afterEach
Outer afterEach
このように、外側のフックと内側のフックが両方とも適用されます。このスコープの仕組みを利用することで、テストのセットアップをより構造化し、管理しやすくできます。
処理が実行される順序
フックとテストが実行される順序を正確に理解することは、意図しない挙動を避けるために重要です。基本的な実行順序は以下の通りです。
beforeAll
(一度だけ実行)- (繰り返し)
beforeEach
test
afterEach
afterAll
(一度だけ実行)
describe
がネストしている場合は、外側の before
から内側の before
へ、内側の after
から外側の after
へと実行されます。
フック関数 | 実行タイミング | 主な用途 |
---|---|---|
beforeAll |
describe ブロック内のすべてのテストが実行される前に一度だけ |
時間のかかる初期設定(DB接続、サーバー起動など) |
afterAll |
describe ブロック内のすべてのテストが実行された後に一度だけ |
リソースのクリーンアップ(DB切断など) |
beforeEach |
describe ブロック内の各テストが実行される直前に毎回 |
テストごとのデータ初期化、モックのリセット |
afterEach |
describe ブロック内の各テストが実行された直後に毎回 |
テストごとのクリーンアップ |
これらのフックを適切に使い分けることで、クリーンでメンテナンスしやすく、信頼性の高いテストスイートを構築できます。
依存関係を分離するモック機能
優れた単体テスト(ユニットテスト)を書くための重要な原則の一つに、「テスト対象の分離(Isolation)」があります。つまり、テストしたい関数やモジュール(ユニット)が、他の部分(依存関係)から影響を受けずに、それ自体のロジックだけを検証できるようにすることです。
しかし、実際のコードでは、多くの関数やモジュールが外部のAPI、データベース、他のモジュール、あるいは時間といった外部要因に依存しています。これらの依存関係をそのままにしてテストを行うと、以下のような問題が発生します。
- 実行速度の低下: 外部APIへのネットワークリクエストは時間がかかります。
- 不安定なテスト: 外部APIのサーバーがダウンしていたり、ネットワークが不安定だったりすると、テスト対象のコードに問題がなくてもテストが失敗します。
- テストの再現性の欠如: 外部APIが返すデータが変化すると、テスト結果も変わってしまいます。
- 特定の状況の再現困難: サーバーエラーやタイムアウトといった異常系の状況を意図的に作り出すのが難しいです。
これらの問題を解決するのが「モック(Mock)」です。
モックとは
モックとは、テスト対象が依存しているモジュールや関数の「偽物」や「代役」のことです。 Jestのモック機能を使うと、これらの依存関係をテスト用に制御可能な偽物に置き換えることができます。
モックを利用する主な目的は以下の通りです。
- 依存関係の排除: 外部APIやDBに実際にアクセスする代わりに、モックが固定のデータを即座に返すようにします。これにより、テストは高速かつ安定します。
- 振る舞いのスパイ: 依存する関数が「正しい引数で」「期待した回数だけ」呼び出されたかどうかを監視(スパイ)します。
- 返り値の操作: 依存する関数が返す値を、テストシナリオに応じて自由に設定できます。成功時のデータや、エラーオブジェクトなどを返すように操作できます。
Jestは、jest.fn()
によるモック関数の作成、jest.mock()
によるモジュール全体のモック化など、非常に強力で使いやすいモック機能を提供しています。
モック関数 (jest.fn) の使い方
jest.fn()
は、最も基本的なモック関数を生成する機能です。この関数は、自身がどのように呼び出されたかを記録する特殊なプロパティ(.mock
)を持っています。
test('jest.fn() の基本的な使い方', () => {
const mockCallback = jest.fn(x => 42 + x);
// コールバック関数を渡して実行
[0, 1].forEach(mockCallback);
// .mock プロパティで呼び出し情報を確認
// .mock.calls は呼び出された際の引数の配列
expect(mockCallback.mock.calls.length).toBe(2); // 2回呼ばれた
expect(mockCallback.mock.calls[0][0]).toBe(0); // 1回目の呼び出しの第一引数は0
expect(mockCallback.mock.calls[1][0]).toBe(1); // 2回目の呼び出しの第一引数は1
// .mock.results は呼び出された際の返り値の配列
expect(mockCallback.mock.results[0].value).toBe(42); // 1回目の返り値は42
});
また、モック関数の返り値をテストの途中で操作することも可能です。
.mockReturnValue(value)
: 常に固定の値を返すように設定します。.mockReturnValueOnce(value)
: 次の一回の呼び出しだけ特定の値を返すように設定します。.mockResolvedValue(value)
: Promiseを返し、value
で解決(resolve)されるように設定します(非同期処理のモック)。.mockRejectedValue(error)
: Promiseを返し、error
で拒否(reject)されるように設定します。
const myMock = jest.fn();
console.log(myMock()); // > undefined
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock()); // > 10, 'x', true, true
モジュールをモックする方法 (jest.mock)
個別の関数だけでなく、モジュール全体をモック化することもできます。これは、axiosのようなHTTPクライアントライブラリや、自作のAPIクライアントモジュールなどをテストする際に非常に便利です。
jest.mock('moduleName')
をテストファイルの先頭(import
やrequire
の直後)に記述すると、指定したモジュールが自動的にモック化されます。そのモジュールからエクスポートされるすべての関数は、jest.fn()
で作られた空のモック関数に置き換えられます。
具体例: axiosを使ったAPIリクエストのテスト
ユーザー情報を取得する Users
クラスをテストするシナリオを考えます。
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
この Users.all
メソッドをテストする際、実際に axios.get
を呼び出してネットワークリクエストを発生させたくありません。そこで axios
をモックします。
// users.test.js
import axios from 'axios';
import Users from './users';
// axiosモジュールをモック化する
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
// モック化されたaxios.getの返り値を設定する
// .mockResolvedValue() を使ってPromiseを返すようにする
axios.get.mockResolvedValue(resp);
// Users.all() を呼び出すと、内部でモック化された axios.get が使われる
return Users.all().then(data => expect(data).toEqual(users));
});
このテストでは、jest.mock('axios')
によって axios
モジュール全体がモックに置き換えられています。axios.get
はもはや実際のHTTPリクエストを行わず、単なるモック関数です。そこで、axios.get.mockResolvedValue(resp)
を使って、このモック関数が呼び出された際に resp
オブジェクトで解決されるPromiseを返すように振る舞いを設定しています。
これにより、ネットワークに依存しない、高速で安定したテストが実現できます。
タイマー関数をモックする (setTimeout, setInterval)
setTimeout
や setInterval
、clearTimeout
といったグローバルなタイマー関数に依存するコードのテストは、通常は待ち時間が発生するため困難です。
Jestは jest.useFakeTimers()
を呼び出すことで、これらのタイマー関数をモックに置き換えることができます。これにより、時間を仮想的に進めることが可能になります。
// timerGame.js
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000); // 1秒後にコールバックを呼ぶ
}
// timerGame.test.js
jest.useFakeTimers(); // タイマーをモック化
test('calls the callback after 1 second', () => {
const callback = jest.fn();
timerGame(callback);
// この時点では、コールバックはまだ呼ばれていない
expect(callback).not.toBeCalled();
// すべてのタイマーを最後まで進める
jest.runAllTimers();
// これでコールバックが呼ばれたはず!
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
jest.useFakeTimers()
を宣言した後、jest.runAllTimers()
を呼び出すと、キューに入っているすべてのタイマー処理が同期的に即時実行されます。これにより、1秒待つ必要なく、setTimeout
の中のコールバックが実行されたことを検証できます。他にも、jest.advanceTimersByTime(msToRun)
で指定した時間だけ進めることも可能です。
モック機能を使いこなすことは、Jestでのテストを一段上のレベルに引き上げる鍵となります。
Jestの便利な追加機能
Jestには、これまで紹介してきた基本的な機能に加えて、開発をさらに効率化し、コードの品質を高めるための便利な機能が数多く搭載されています。ここでは、特に代表的な「スナップショットテスト」と「テストカバレッジ」について詳しく解説します。
スナップショットテストでUIの変更を検知する
スナップショットテストは、大規模なオブジェクトやUIコンポーネントの構造が意図せず変更されていないかを保証するための非常に強力なツールです。
テスト対象の出力(ReactコンポーネントのDOM構造、APIからの巨大なJSONレスポンスなど)が頻繁に変わる、あるいは非常に大きい場合、toEqual
を使って期待値を手で書くのは現実的ではありません。スナップショットテストは、この問題を解決します。
仕組み:
- テストで初めて
.toMatchSnapshot()
マッチャーが実行されると、Jestはテスト対象の出力結果(シリアライズされた文字列)を スナップショットファイル (.snap
拡張子) として__snapshots__
ディレクトリに保存します。このファイルが「正解」の記録となります。 - 2回目以降のテスト実行時には、現在の出力結果と、保存されているスナップショットファイルの内容を比較します。
- 両者が一致していればテストは成功します。一致しない場合、テストは失敗し、どこがどのように違うのかという差分(diff)が表示されます。
具体例:Reactコンポーネントのテスト
シンプルなリンクコンポーネントをテストする例を見てみましょう。
// Link.js (React Component)
import React from 'react';
const Link = ({page, children}) => (
<a href={page}>
{children}
</a>
);
export default Link;
このコンポーネントをテストするために、react-test-renderer
というライブラリを使ってコンポーネントをJSON形式に変換し、スナップショットを撮ります。
// Link.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Link from './Link';
test('Link component renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
// .toMatchSnapshot() を呼び出す
expect(tree).toMatchSnapshot();
});
初回 npm test
実行時:
Jestは Link.test.js.snap
というファイルを生成します。中身は以下のようになります。
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link component renders correctly 1`] = `
<a
href="http://www.facebook.com"
>
Facebook
</a>
`;
コンポーネントを誤って変更した場合:
もし Link.js
の <a>
タグに誤って className="my-link"
を追加してしまったとします。その状態で再度テストを実行すると、テストは失敗し、以下のような差分が表示されます。
- Snapshot
+ Received
<a
+ className="my-link"
href="http://www.facebook.com"
>
Facebook
</a>
この差分を見て、変更が意図したものなのか、意図しないバグなのかを判断します。
- 意図した変更の場合: Watchモードで
u
キーを押すか、jest --updateSnapshot
コマンドを実行して、スナップショットファイルを新しい内容で更新します。 - 意図しない変更の場合: コードを修正して、元のスナップショットと一致するようにします。
スナップショットテストは、UIのデグレード防止や、APIレスポンスのスキーマ変更の検知に絶大な効果を発揮します。ただし、スナップショットを安易に更新し続けるとテストの意味がなくなるため、差分をきちんとレビューする運用が不可欠です。
テストカバレッジを計測する方法
テストカバレッジ(Test Coverage)とは、作成したコードのうち、どれだけの割合が自動テストによって実行されたかを示す指標です。 カバレッジを計測することで、テストが手薄な箇所や、全くテストされていないコードを可視化できます。
Jestでは、テスト実行時に --coverage
オプションを付けるだけで、簡単にカバレッジレポートを生成できます。
npm test -- --coverage
# または package.json の scripts に登録したコマンドを実行
# npm run test:coverage
このコマンドを実行すると、テストの実行結果に加えて、ターミナルにカバレッジのサマリーテーブルが表示されます。
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 77.77 | 50 | 66.66 | 77.77 |
sum.js | 50 | 0 | 50 | 50 | 3-5
users.js | 100 | 100 | 100 | 100 |
----------------|---------|----------|---------|---------|-------------------
さらに、プロジェクトのルートに coverage
ディレクトリが生成され、その中に lcov-report/index.html
というHTML形式の詳細なレポートが作成されます。このHTMLファイルをブラウザで開くと、ファイルごと、行ごとにどのコードが実行されたか(緑色)、実行されなかったか(赤色)を視覚的に確認できます。
カバレッジの指標:
- % Stmts (Statements): ステートメント(文)の網羅率。
- % Branch (Branches):
if
文や三項演算子などの分岐(ブランチ)の網羅率。if
とelse
の両方のパスがテストされているか。 - % Funcs (Functions): 関数の網羅率。
- % Lines (Lines): コードの行の網羅率。
注意点:
テストカバレッジ100%は、必ずしも完璧なテストを意味しません。 例えば、テストがコードのすべての行を実行したとしても、あらゆる入力パターンやエッジケースを検証しているとは限りません。カバレッジはあくまで「テストされていない箇所」を見つけるためのツールであり、目標数値を達成すること自体が目的ではありません。
品質保証の仕組みとして、jest.config.js
に coverageThreshold
を設定することもできます。
// jest.config.js
module.exports = {
// ...
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: -10, // 10%低下までは許容
},
},
};
これにより、カバレッジが設定した閾値を下回った場合にテストを失敗させ、コード品質の低下を防ぐことができます。
まとめ
本記事では、JavaScriptのテストフレームワークであるJestについて、その基本的な概念から具体的な導入方法、応用的な機能までを網羅的に解説してきました。
最後に、この記事で学んだ重要なポイントを振り返りましょう。
- Jestはオールインワンで高機能なフレームワーク: ゼロコンフィグで簡単に導入でき、テストランナー、アサーション、モック、カバレッジといったテストに必要な機能がすべて揃っています。これにより、開発者は環境構築の手間なく、すぐにテストコードの記述に集中できます。
- 基本的なテストの構造はシンプル:
describe
でテストをグループ化し、test
(またはit
)で個々のケースを定義し、expect
とマッチャーで検証を行うという直感的な構造をしています。コードがドキュメントのように機能し、可読性の高いテストを書くことができます。 - 豊富なマッチャーがテストの表現力を高める:
.toBe
や.toEqual
といった基本的なものから、.toThrow
や.toContain
まで、様々な状況に対応するマッチャーが用意されており、テストの意図を明確に表現できます。 - 非同期処理や依存関係も確実にテスト可能:
async/await
構文を使えば、Promiseベースの非同期処理を簡単かつ直感的にテストできます。また、jest.mock
を活用することで、外部APIやデータベースといった依存関係を切り離し、高速で安定した単体テストを実現できます。 - スナップショットテストとカバレッジが品質を支える: スナップショットテストはUIコンポーネントなどの意図しない変更を検知する強力な武器となり、テストカバレッジはテストが不足している箇所を可視化し、テストの網羅性を高めるのに役立ちます。
Jestをプロジェクトに導入し、テストを記述する文化を根付かせることは、単にバグを減らすだけでなく、多くのメリットをもたらします。リファクタリングへの心理的なハードルが下がり、コードの改善が促進されます。テストコードは生きた仕様書として機能し、チームメンバー間のコミュニケーションを円滑にします。そして何より、開発者は自信を持ってコードをリリースできるようになります。
もしあなたがこれまでテストを書いたことがなくても、心配する必要はありません。まずはこの記事で紹介したような、簡単な純粋関数に対するテストから始めてみましょう。小さな成功体験を積み重ねることで、徐々にテストを書くことの楽しさとその価値を実感できるはずです。
この記事が、あなたがJestを学び、品質の高いJavaScriptアプリケーションを開発するための一助となれば幸いです。