今回は、Jestについてまとめていきます。TypeScriptをベースにJestを使ったテスト環境を構築する手順をまとめていきます。
Jestについて
Jestとは
JestはJavaScriptおよびTypeScriptのテストフレームワークで、Facebookによって開発されました。主にReactアプリケーションのテストに用いられ、Node.js環境でも利用可能です。Jestは簡潔な構文と豊富な機能を備え、テストスイートを作成し、テストケースを実行するためのツールです。また、モックやスパイ機能もサポートしており、テストの自動化やコードカバレッジの計測にも使われます。
Jestの特徴
- 簡単なセットアップ:Jestは簡単にセットアップでき、ほとんどの場合、追加の設定が不要です。TypeScriptプロジェクトにも統合しやすい特長がある。
- スナップショットテスト: UIコンポーネントのスナップショットテストをサポートし、視覚的な変更を検出が可能。
- モックとスパイ:Jestはモックとスパイを容易に作成でき、外部依存関係をシミュレートするのに便利。
- 並列実行:マルチプロセスでテストケースを並列実行し、高速なテスト実行を実現する。
- 豊富なプラグイン:多くのプラグインや拡張機能が利用可能で、カスタマイズ性が高い。
実装の流れ
前回の記事を参考にNode.js + TypeScriptの実行環境を構築
開発環境
- Node.js:
v18.15.0
- npm:
v9.5.0
- TypeScript:
v5.1.6
- Jest:
v29.7.0
- OS:
MacOS Monterey
構築手順
今回の構築手順は下記を参考にしていますので、詳しくはサイトをご覧ください。
実行環境の構築
まずはNode.js + TypeScriptで実行できる環境を構築。
構築方法は以下をベースに実施しましたので、まだ構築できていない方は参考にしてください。
Jestのインストール
$ npm install --save-dev jest ts-jest @types/jest
- ts-jest:TypeScriptでJestを使用するためのツール。Jestで実行可能なJavaScriptにトランスパイルする。
- @types/jest:Jestの型定義ファイル。TypeScriptでJestを型安全に使用するために必要。
https://jestjs.io/ja/docs/getting-started#typescript-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B
@types/jest
はDefinitelyTypedでメンテナンスされているサードパーティライブラリで、最新のJestの機能やバージョンがまだカバーされていないことがあります。したがって、Jestと@types/jest
のバージョンをできるだけ近づけるようにしましょう。たとえば、Jestのバージョンが27.4.0の場合、@types/jest
のバージョンを27.4.xとするのが理想的です。
package.json
のscripts
にテスト用コマンドを追加する
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
},
Jestの設定ファイル作成
jest.config.tsファイルの作成
$ npx ts-jest config:init
上記コマンドではjest.config.js
ファイルが作成されますが、今回はtsファイルとして作成し、ts-jest
をベースに実装していきます。
import { type JestConfigWithTsJest } from 'ts-jest';
import { defaults as tsjPreset } from 'ts-jest/presets';
const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true,
roots: ['<rootDir>'],
transform: {
...tsjPreset.transform,
},
testMatch: ['<rootDir>/test/**/*.test.ts'],
modulePaths: ['<rootDir>/src'],
moduleNameMapper: {
'^@/(.+)': '<rootDir>/src/$1',
'^@test/(.+)': '<rootDir>/test/$1',
},
moduleDirectories: ['node_modules', '<rootDir>'],
};
export default jestConfig;
各種設定の解説
設定項目 | 説明 | 効果 |
---|---|---|
preset | TypeScript用のJestのプリセットを指定する。 | TypeScriptのコードをJestで実行できるようにする。 |
testEnvironment | テストの実行環境を指定する。 | node を指定することで、Node.js環境でテストが実行される。 |
verbose | テストの詳細な情報を表示するかどうかを指定する。 | エラーメッセージやテスト結果などがより詳細に表示される。 |
roots | テスト対象のファイルやディレクトリのルートパスを指定する。 | テストファイルがどのディレクトリから始まるかを定義する。 |
transform | ファイルの変換設定を行う。 | TypeScriptのコードをJestが理解できる形に変換する。 |
testMatch | テストファイルのパターンを指定する。 | 指定したパターンに一致するファイルがテスト対象となる。 |
modulePaths | モジュールの探索パスを指定する。 | 指定したディレクトリからモジュールを探索する。 |
moduleNameMapper | モジュール名のエイリアスを設定する。 | 特定のモジュール名を別のパスにマッピングする。 |
moduleDirectories | モジュールのディレクトリを指定する。 | 指定したディレクトリからモジュールを探索する。 |
テストコードの実装
最終的なディレクトリ構成は以下
.
├── node_modules
├── dist
│ └── index.js
├── src
│ └── sample.ts
├── test
│ └── sample.test.ts
├── tsconfig.json
├── jest.config.ts
├── package-lock.json
└── package.json
実行ファイルの実装
テストコードのための簡易的な関数を実装します。
例外処理のテストも実装したかったので、無理やり例外処理を入れ込んでいます。
/**
* メッセージを生成する
* @param {string} text - メッセージに含める文字列
* @throws {Error} - textが空文字またはNGを含む場合にエラーを投げる
* @returns {string} - 生成されたメッセージ
*/
export const generateMessage = (text: string): string => {
if (!text || text.includes('NG')) {
throw new Error('text is invalid');
}
return 'Hello, ' + text + '!';
};
テストコードの実装
下記3パターンを実装しています。
- 正常系:与えたtextを使ってメッセージが生成されること
- 異常系:与えたtextが空文字の場合はエラーが返ってくること
- 異常系:与えたtextに「NG」が含まれている場合はエラーが返ってくること
import { generateMessage } from '@/sample';
describe('generateMessage', () => {
it('should return 「Hello, World!」', () => {
const actual = generateMessage('World');
expect(actual).toBe('Hello, World!');
});
it('should throw an error if text is empty', () => {
expect(() => generateMessage('')).toThrowError('text is invalid');
});
it('should throw an error if text includes NG', () => {
expect(() => generateMessage('NG')).toThrowError('text is invalid');
});
});
動作検証
package.jsonのscriptsに追加したコマンドでテストを実行します。
$ npm run test
> ts-jest-sample@1.0.0 test
> jest
PASS test/sample.test.ts
generateMessage
✓ should return 「Hello, World!」 (3 ms)
✓ should throw an error if text is empty (5 ms)
✓ should throw an error if text includes NG (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.181 s
Ran all test suites.
無事にテストを動かすところまで実装できました。
ちなみにJestをグローバルにインストールしていれば、jestコマンドで実行することも可能です。
$ npm install -g jest
$ jest
PASS test/sample.test.ts
generateMessage
✓ should return 「Hello, World!」 (2 ms)
✓ should throw an error if text is empty (6 ms)
✓ should throw an error if text includes NG (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.915 s, estimated 1 s
テスト対象のファイルを指定することも可能です。
$ npm run test test/sample.test.ts
$ jest test/sample.test.ts
Jestに関するその他情報
Jestで使用する関数一覧(一部)
describe
、it
、test
などは、Jestにおけるテストの構造を定義するための関数です。
簡単に一部を一覧化してみました。
メソッド | 用途 | 構文 |
---|---|---|
describe | テストスイート(関連するテストケース)を定義 | describe(name: string, fn: () => void) |
it / test | 単一のテストケースを定義 | it(name: string, fn: () => void) |
beforeEach | 各テストケースが実行される前に実行される関数を定義 | beforeEach(fn: () => void) |
afterEach | 各テストケースが実行された後に実行される関数を定義 | afterEach(fn: () => void) |
beforeAll | 最初のテストケースが実行される前に一度だけ実行される関数を定義 | beforeAll(fn: () => void) |
afterAll | 全てのテストケースが実行された後に一度だけ実行される関数を定義 | afterAll(fn: () => void) |
skip | テストケースやテストスイートをスキップ | describe.skip / it.skip / test.skip |
xit / xtest | it / test のスキップ版 | xit(name: string, fn: () => void) |
xxx.only | 指定したテストスイートだけを実行 | describe.only(name: string, fn: () => void) ※ it、testも同様 |
xxx.each | 同じテストケースを複数の異なる引数で繰り返し実行 | describe.each(table)(name, fn) ※ it、testも同様 |
テストコードに対するリンターやフォーマッター
テストコードに対する品質管理も極めて重要です。プロダクションコードに対して使っているESLintやPrettierなどをテストコードに対して適用することも可能です。
詳しくはここで説明しませんが、例として、test
ではなくit
を使うようにESLintで制御したり、スペースや行数制限などをPrettierで制御したりすることができます。
Jestでの導入記事ではないですが、以前書いたESLint & Prettierについての記事を貼っておきます。
マッチャーについて
Jestのマッチャーは、テストケース内で期待する結果を検証するために使用されます。例えば、expect(result).toBe(3)
の場合、toBe
がマッチャーです。他にもさまざまなマッチャーがあり、例えば、toEqual
はオブジェクトや配列の内容を比較します。他にもtoThrowError
などで例外処理のテストを書くことも可能です。これにより、テストケースが期待通りの結果を返すかどうかを簡潔に確認できます。
モックについて
モックは、テスト中に本物のオブジェクトや機能を置き換える仕組みです。これにより、テストをより制御可能にし、依存関係の解決や外部リソースへのアクセスをシミュレートできます。Jestでは、jest.mock()
関数を使用してモックを作成し、実際のオブジェクトやモジュールをテスト用のバージョンで置き換えます。例えば、データベースへの接続や外部APIへの疎通をモックすることで、テストを独立させて実行することが可能です。
直列実行、並列実行について
Jestではデフォルトで並列実行が適用されます。そのため、直列実行したい場合は--runInBand
オプションを付与する必要があります。
例えば、テスト用DBを起動させて、実際にDBに対して書き込み/読み込みを行うようなテストを動かす場合は、直列実行でないと正常に動作しないことが多いかと思います。
$ npm run test --runInBand
ちなみに、テストファイルは並列実行されるが、ファイル内の各テストは上から順番に実行されるため、各テストを並列実行したい場合はit.concurrent
のように書く必要があります。(公式Doc)
以下の記事に詳しく解説されていましたので、ぜひ参考にしてください。
カバレッジの算出
Jestでは簡単にテストカバレッジを算出することが可能です。
$ jest --coverage
PASS test/sample.test.ts
generateMessage
✓ should return 「Hello, World!」 (3 ms)
✓ should throw an error if text is empty (7 ms)
✓ should throw an error if text includes NG (1 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
sample.ts | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.315 s
Ran all test suites.
テスト実行後にcoverage
ディレクトリが作成され、その中のicov-report/index.html
をブラウザで開くとより詳細なカバレッジ情報を確認することが可能です。
どの行を通っている/通っていないを確認できるため、テストケースの実装漏れにも気づけます。
まとめ
今回はJestにおけるテスト環境構築や基本構造についてまとめてみました。今までなんとなく使っていた部分が多かったため、自分自身も良い機会となりました。個人的にはテストが書いていないと怖くて機能追加の実装やリファクタができないので、自分の中ではテストの優先度はかなり高いです。
そのため、今後もモックやマッチャーについてはさらに詳しくまとめていきたいと思います。
ご覧いただきありがとうございました。