Jest の並列実行はなぜ速い?実行キューとワーカーの舞台裏を読み解く
Jest でテストを実行すると、あっという間に数百のテストケースが完了しますよね。この驚異的な速度の秘密は「並列実行」にあります。
本記事では、Jest がどのように並列実行を実現しているのか、実行キューとワーカーの仕組みを深掘りしていきましょう。Jest の内部アーキテクチャを理解することで、テストパフォーマンスの最適化やカスタマイズがより効果的に行えるようになります。
背景
Jest の並列実行とは
Jest は、デフォルトで複数のテストファイルを並列に実行する機能を持っています。これにより、テストスイート全体の実行時間を大幅に短縮できるのです。
従来のテストランナーでは、テストファイルを順番に 1 つずつ実行していました。100 個のテストファイルがあり、それぞれが 1 秒かかる場合、全体で 100 秒必要になります。
一方、Jest は複数のワーカープロセスを起動し、テストファイルを分散して実行するため、マルチコアの CPU を効率的に活用できます。4 コアの CPU であれば、理論上は実行時間を約 4 分の 1 に短縮できるでしょう。
Jest のプロセス構造
Jest は以下の 2 種類のプロセスで構成されます。
-
親プロセス(メインプロセス)
- テストファイルの収集と分析
- ワーカープロセスの管理
- 実行キューの制御
- テスト結果の集約とレポート出力
-
ワーカープロセス(子プロセス)
- 実際のテストファイルの実行
- テスト環境のセットアップ
- テスト結果の親プロセスへの送信
次の図は、Jest の基本的なプロセス構造を示しています。
mermaidflowchart TB
main["親プロセス<br/>(jest-cli)"]
queue["実行キュー<br/>(TestQueue)"]
scheduler["スケジューラ<br/>(TestScheduler)"]
worker1["ワーカー1<br/>(jest-worker)"]
worker2["ワーカー2<br/>(jest-worker)"]
worker3["ワーカー3<br/>(jest-worker)"]
worker4["ワーカー4<br/>(jest-worker)"]
main -->|"テストファイル<br/>リスト作成"| queue
queue -->|"タスク割り当て"| scheduler
scheduler -->|"テスト実行<br/>指示"| worker1
scheduler -->|"テスト実行<br/>指示"| worker2
scheduler -->|"テスト実行<br/>指示"| worker3
scheduler -->|"テスト実行<br/>指示"| worker4
worker1 -->|"結果送信"| main
worker2 -->|"結果送信"| main
worker3 -->|"結果送信"| main
worker4 -->|"結果送信"| main
この図から、親プロセスが複数のワーカーを統括し、効率的にタスクを分散していることが分かります。親プロセスはテストファイルをキューに積み、各ワーカーが空き次第、次のテストファイルを割り当てていくのです。
なぜ並列実行が必要なのか
現代の Web アプリケーション開発では、テストケースの数が膨大になりがちです。React コンポーネント、API エンドポイント、ビジネスロジック、ユーティリティ関数など、様々なレイヤーでテストを書くことが推奨されていますね。
テストケースが増えれば増えるほど、実行時間も比例して増加します。CI/CD パイプラインでテストに 10 分も 20 分もかかってしまうと、開発フィードバックのサイクルが遅くなり、生産性が低下してしまうでしょう。
並列実行は、この課題に対する最も効果的な解決策の 1 つです。ハードウェアのリソースを最大限活用することで、開発者の待ち時間を最小化できます。
課題
テスト実行の主な課題
並列実行を実現するには、いくつかの技術的な課題をクリアする必要があります。
課題 1:プロセス間通信
親プロセスとワーカープロセスは別々のプロセスとして動作するため、通信方法が必要です。どのテストファイルを実行すべきか、実行結果はどうだったか、といった情報を効率的にやり取りする仕組みが求められます。
課題 2:タスクの公平な分散
テストファイルごとに実行時間が異なります。1 秒で終わるものもあれば、10 秒かかるものもあるでしょう。単純に均等に分割すると、あるワーカーは早々に終わってしまい、別のワーカーはまだ重いテストを実行中、という状況が発生します。
課題 3:リソース管理
ワーカープロセスを無制限に起動すると、メモリ不足やコンテキストスイッチのオーバーヘッドで逆に遅くなってしまいます。適切なワーカー数を決定し、リソースを効率的に管理する必要があります。
課題 4:テスト間の独立性
並列実行では、複数のテストが同時に実行されます。あるテストが他のテストに影響を与えてしまうと(例:共有されたグローバル変数の変更、データベースの状態変更など)、テスト結果が不安定になってしまうでしょう。
次の図は、並列実行における主な課題を示しています。
mermaidflowchart TD
start["並列実行の課題"]
challenge1["プロセス間<br/>通信"]
challenge2["タスクの<br/>公平な分散"]
challenge3["リソース<br/>管理"]
challenge4["テスト間の<br/>独立性"]
detail1["IPC の実装<br/>メッセージの<br/>シリアライズ"]
detail2["実行時間の<br/>予測不可能性<br/>負荷の偏り"]
detail3["メモリ消費<br/>CPU 使用率<br/>最適なワーカー数"]
detail4["グローバル状態<br/>ファイルシステム<br/>データベース"]
start --> challenge1
start --> challenge2
start --> challenge3
start --> challenge4
challenge1 --> detail1
challenge2 --> detail2
challenge3 --> detail3
challenge4 --> detail4
これらの課題に対して、Jest は独自のアーキテクチャで解決策を提供しています。特に実行キューとワーカーの設計が、並列実行の効率性と安定性を支えているのです。
解決策
Jest の並列実行アーキテクチャ
Jest は、jest-runner と jest-worker という 2 つの主要パッケージを使って並列実行を実現しています。これらがどのように連携して、先ほどの課題を解決しているのか見ていきましょう。
1. jest-worker による Worker Pool パターン
Jest は jest-worker パッケージで Worker Pool パターンを実装しています。これは、あらかじめ一定数のワーカープロセスを起動しておき、タスクが来たら空いているワーカーに割り当てるという仕組みです。
ワーカー数の決定
Jest は、デフォルトで以下のロジックでワーカー数を決定します。
typescript// Jest のワーカー数決定ロジック(簡略版)
import * as os from 'os';
function getMaxWorkers(userConfig?: number): number {
// ユーザーが明示的に指定した場合はそれを使用
if (userConfig !== undefined) {
return userConfig;
}
// CPU コア数を取得
const cpuCount = os.cpus().length;
typescript // CI 環境の検出
const isCI = process.env.CI === 'true';
if (isCI) {
// CI 環境では CPU コア数 - 1(最低 2)
return Math.max(cpuCount - 1, 2);
} else {
// ローカル環境では CPU コア数の 50%(最低 1)
return Math.max(Math.floor(cpuCount / 2), 1);
}
}
このコードでは、まずユーザー設定を確認し、指定がなければ環境に応じて自動計算しています。CI 環境ではより多くのワーカーを使い、ローカル開発環境では控えめにすることで、他のアプリケーションへの影響を最小限に抑えているのです。
Worker Pool の初期化
jest-worker は以下のように Worker Pool を初期化します。
typescript// jest-worker の基本的な使い方
import { Worker } from 'jest-worker';
// ワーカープールの作成
const worker = new Worker(
require.resolve('./worker.js'), // ワーカーが実行するスクリプト
{
numWorkers: 4, // ワーカー数
enableWorkerThreads: false, // プロセスモードを使用
forkOptions: {
stdio: 'pipe', // 標準入出力の設定
serialization: 'json', // メッセージのシリアライズ形式
},
}
);
このコードで、4 つのワーカープロセスが起動されます。enableWorkerThreads を false にすることで、Node.js の child_process.fork() を使ったプロセスベースの並列実行が行われるのです。
2. 実行キューによるタスク管理
Jest は、テストファイルを実行キューで管理します。この仕組みにより、タスクの公平な分散を実現しているのです。
TestScheduler の役割
jest-runner の TestScheduler クラスが、実行キューの中核を担っています。
typescript// TestScheduler の基本構造(簡略版)
class TestScheduler {
private _testQueue: Array<TestEntry>;
private _worker: Worker;
constructor(
globalConfig: GlobalConfig,
context: TestContext
) {
this._testQueue = [];
this._worker = new Worker(
require.resolve('./testWorker'),
{
numWorkers: globalConfig.maxWorkers,
}
);
}
typescript // テストファイルをキューに追加
addToQueue(test: TestEntry): void {
this._testQueue.push(test);
}
// キューからテストを取り出してワーカーに割り当て
async scheduleTests(): Promise<AggregatedResult> {
const results: Array<TestResult> = [];
typescript // 各テストファイルを順次実行
for (const test of this._testQueue) {
try {
// ワーカーにテスト実行を依頼
const result = await this._worker.runTest(test);
results.push(result);
} catch (error) {
// エラーハンドリング
results.push(createFailedTestResult(test, error));
}
}
return aggregateResults(results);
}
}
このコードでは、テストファイルをキューに積み、ワーカーに順次割り当てています。await this._worker.runTest(test) の部分で、空いているワーカーが自動的に選ばれ、実行されるのです。
動的タスク割り当て
Jest の実行キューは「動的タスク割り当て」を採用しています。これは、事前にどのワーカーがどのテストを実行するか決めるのではなく、ワーカーが空き次第、次のタスクを割り当てる方式です。
次の図は、動的タスク割り当ての流れを示しています。
mermaidsequenceDiagram
participant Queue as 実行キュー
participant Scheduler as スケジューラ
participant W1 as ワーカー1
participant W2 as ワーカー2
Scheduler->>Queue: テストファイル登録<br/>(test1.js, test2.js, test3.js)
Scheduler->>W1: test1.js を実行
Scheduler->>W2: test2.js を実行
Note over W1,W2: 並列実行中
W1->>Scheduler: test1.js 完了<br/>(実行時間: 2秒)
Scheduler->>W1: test3.js を実行
W2->>Scheduler: test2.js 完了<br/>(実行時間: 5秒)
Note over W2: アイドル状態
W1->>Scheduler: test3.js 完了<br/>(実行時間: 1秒)
Scheduler->>Scheduler: 全テスト完了<br/>結果を集約
この図から、実行時間が異なるテストでも、空いたワーカーに次のタスクを即座に割り当てることで、効率的に実行できることが分かります。test1.js が早く終わったワーカー 1 は、すぐに test3.js の実行に移れるのです。
3. プロセス間通信の実装
Jest は Node.js の child_process モジュールを使って、親プロセスとワーカープロセス間の通信を行っています。
メッセージの送受信
親プロセスからワーカーへのメッセージ送信は以下のように実装されます。
typescript// 親プロセス側:ワーカーにテスト実行を依頼
import { fork, ChildProcess } from 'child_process';
class WorkerProcess {
private _child: ChildProcess;
private _callbacks: Map<number, Function>;
private _requestId: number;
constructor(workerPath: string) {
this._child = fork(workerPath, [], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});
this._callbacks = new Map();
this._requestId = 0;
// ワーカーからのメッセージを受信
this._child.on('message', this._onMessage.bind(this));
}
typescript // テスト実行リクエストの送信
async runTest(testPath: string): Promise<TestResult> {
return new Promise((resolve, reject) => {
const requestId = this._requestId++;
// コールバックを登録
this._callbacks.set(requestId, { resolve, reject });
// ワーカーにメッセージを送信
this._child.send({
type: 'run-test',
requestId,
testPath,
});
});
}
typescript // ワーカーからのメッセージを処理
private _onMessage(message: any): void {
const { requestId, type, result, error } = message;
const callback = this._callbacks.get(requestId);
if (!callback) {
return;
}
this._callbacks.delete(requestId);
if (type === 'test-success') {
callback.resolve(result);
} else if (type === 'test-failure') {
callback.reject(error);
}
}
}
このコードでは、requestId を使ってリクエストとレスポンスを紐付けています。非同期で複数のテストを実行しても、結果を正しく対応させられるのです。
ワーカー側の実装は以下のようになります。
typescript// ワーカープロセス側:テストを実行して結果を返す
import { runTest } from '@jest/core';
// 親プロセスからのメッセージを受信
process.on('message', async (message: any) => {
const { type, requestId, testPath } = message;
if (type !== 'run-test') {
return;
}
try {
// テストを実行
const result = await runTest(testPath);
typescript // 成功結果を親プロセスに送信
process.send({
type: 'test-success',
requestId,
result,
});
} catch (error) {
// エラーを親プロセスに送信
process.send({
type: 'test-failure',
requestId,
error: serializeError(error),
});
}
});
ワーカーは受け取ったテストパスに基づいてテストを実行し、結果を親プロセスに返します。エラーが発生した場合は、シリアライズしてから送信することで、プロセス間でも正しく情報を伝達できるのです。
4. テストの独立性確保
Jest は、各テストファイルを独立したプロセスで実行することで、テスト間の独立性を保証しています。
プロセス分離によるメリット
typescript// 各ワーカープロセスは独立した環境を持つ
// worker1.js
global.testCounter = 0; // ワーカー1 のグローバル変数
// worker2.js
global.testCounter = 0; // ワーカー2 のグローバル変数(独立)
プロセスが分離されているため、あるテストでグローバル変数を変更しても、他のテストには影響しません。各ワーカーは独自のメモリ空間を持っているからです。
テスト環境のリセット
Jest は各テストファイルの実行前に、テスト環境をリセットします。
typescript// テスト環境のセットアップとクリーンアップ
import { JestEnvironment } from '@jest/environment';
class TestRunner {
async runTestFile(testPath: string): Promise<TestResult> {
// 新しいテスト環境を作成
const environment = new JestEnvironment(this._config);
try {
// テスト環境をセットアップ
await environment.setup();
typescript // テストファイルを実行
const result = await this._executeTest(testPath, environment);
return result;
} finally {
// テスト環境をクリーンアップ
await environment.teardown();
}
}
}
このコードでは、各テストファイルごとに新しい環境を作成し、実行後は必ずクリーンアップしています。finally ブロックを使うことで、エラーが発生してもクリーンアップが確実に実行されるのです。
5. 実行順序の最適化
Jest は、テストファイルの実行順序も最適化しています。過去の実行時間データを活用して、重いテストを優先的に実行することで、全体の実行時間を短縮するのです。
テスト実行時間のキャッシング
typescript// テスト実行時間をキャッシュする仕組み
import * as fs from 'fs';
import * as path from 'path';
class TestTimingCache {
private _cacheFile: string;
private _timings: Map<string, number>;
constructor(cacheDir: string) {
this._cacheFile = path.join(cacheDir, 'test-timings.json');
this._timings = this._loadCache();
}
typescript // キャッシュからテスト実行時間を読み込む
private _loadCache(): Map<string, number> {
try {
if (fs.existsSync(this._cacheFile)) {
const data = fs.readFileSync(this._cacheFile, 'utf-8');
const json = JSON.parse(data);
return new Map(Object.entries(json));
}
} catch (error) {
// キャッシュの読み込みに失敗しても続行
console.warn('Failed to load test timing cache:', error);
}
return new Map();
}
typescript // テスト実行時間を記録
recordTiming(testPath: string, duration: number): void {
this._timings.set(testPath, duration);
}
// テスト実行時間を取得(デフォルトは 0)
getTiming(testPath: string): number {
return this._timings.get(testPath) || 0;
}
// キャッシュをファイルに保存
save(): void {
const json = Object.fromEntries(this._timings);
fs.writeFileSync(
this._cacheFile,
JSON.stringify(json, null, 2)
);
}
}
このコードでは、各テストファイルの実行時間を JSON ファイルにキャッシュしています。次回のテスト実行時に、この情報を使って最適な順序を決定できるのです。
実行順序のソート
キャッシュされた実行時間を使って、テストファイルをソートします。
typescript// テストファイルを実行時間の降順にソート
class TestScheduler {
private _cache: TestTimingCache;
sortTestsByTiming(tests: Array<Test>): Array<Test> {
return tests.sort((a, b) => {
const timingA = this._cache.getTiming(a.path);
const timingB = this._cache.getTiming(b.path);
// 実行時間が長いテストを優先
return timingB - timingA;
});
}
}
実行時間が長いテストを優先的に実行することで、ワーカーが最後に短いテストだけを残して待機する状況を避けられます。これにより、全体の実行時間を最小化できるのです。
次の図は、実行順序の最適化の効果を示しています。
mermaidflowchart LR
subgraph before["最適化前(8秒)"]
direction TB
w1a["W1: 1秒+1秒+1秒"]
w2a["W2: 5秒"]
end
subgraph after["最適化後(6秒)"]
direction TB
w1b["W1: 5秒+1秒"]
w2b["W2: 1秒+1秒"]
end
before -.->|"実行順序<br/>を最適化"| after
最適化前は、5 秒かかるテストがワーカー 2 に割り当てられたため、ワーカー 1 が先に終わっても待機時間が発生していました。最適化後は、重いテストを優先的に実行することで、両方のワーカーがほぼ同時に完了し、全体の実行時間が短縮されているのです。
具体例
実際の Jest 設定での並列実行制御
ここでは、実際のプロジェクトで Jest の並列実行をどのように制御・最適化するか、具体的な例を見ていきましょう。
例 1:基本的なワーカー数の設定
Jest の設定ファイルで、ワーカー数を明示的に指定できます。
typescript// jest.config.js
module.exports = {
// ワーカー数を 4 に固定
maxWorkers: 4,
// または CPU コア数の 50% を使用
maxWorkers: '50%',
// テストタイムアウトの設定
testTimeout: 30000, // 30 秒
};
このように、maxWorkers オプションで数値または割合を指定できます。CI 環境では固定値、ローカル開発では割合を使うのが一般的です。
例 2:コマンドラインでのワーカー数指定
テスト実行時に、コマンドラインからワーカー数を指定することもできます。
bash# ワーカー数を 2 に制限して実行
yarn jest --maxWorkers=2
# シングルスレッドで実行(デバッグ時に便利)
yarn jest --runInBand
--runInBand オプションを使うと、並列実行を無効化して、すべてのテストを親プロセスで順次実行します。デバッガーを使いたい場合や、テストの失敗原因を特定したい場合に有用です。
例 3:カスタム Test Runner の実装
Jest の並列実行の仕組みを理解すると、カスタム Test Runner を実装できます。
typescript// custom-runner.ts
import { Test, TestResult } from '@jest/test-result';
import { Config } from '@jest/types';
import { Worker } from 'jest-worker';
class CustomTestRunner {
private _globalConfig: Config.GlobalConfig;
private _worker: Worker;
constructor(globalConfig: Config.GlobalConfig) {
this._globalConfig = globalConfig;
typescript // ワーカープールを作成
this._worker = new Worker(
require.resolve('./worker'),
{
numWorkers: globalConfig.maxWorkers,
exposedMethods: ['runTest'],
}
);
}
async runTests(
tests: Array<Test>,
watcher: any,
onStart: any,
onResult: any,
onFailure: any
): Promise<void> {
typescript // テストを実行時間でソート
const sortedTests = this._sortTestsByTiming(tests);
// 各テストを順次実行(ワーカーが自動的に割り当てられる)
for (const test of sortedTests) {
// テスト開始を通知
if (onStart) {
await onStart(test);
}
try {
// ワーカーでテストを実行
const result = await this._worker.runTest(test.path);
typescript // 結果を記録
if (onResult) {
await onResult(test, result);
}
} catch (error) {
// エラーを処理
if (onFailure) {
await onFailure(test, error);
}
}
}
}
private _sortTestsByTiming(tests: Array<Test>): Array<Test> {
// 過去の実行時間データを使ってソート
// 実装は前述の TestTimingCache を参照
return tests;
}
}
module.exports = CustomTestRunner;
このカスタムランナーでは、テストの開始・結果・失敗時にコールバックを呼び出せるようにしています。これにより、独自のレポーターやモニタリングツールと統合できるのです。
例 4:Worker ファイルの実装
カスタムランナーで使用するワーカーファイルも実装してみましょう。
typescript// worker.ts
import { runCLI } from '@jest/core';
import type { Config } from '@jest/types';
interface WorkerOptions {
config: Config.ProjectConfig;
globalConfig: Config.GlobalConfig;
}
export async function runTest(
testPath: string
): Promise<any> {
// Jest の CLI を使ってテストを実行
const result = await runCLI(
{
_: [testPath],
config: JSON.stringify(this.config),
} as Config.Argv,
[this.config.rootDir]
);
typescript // 結果を整形して返す
return {
testFilePath: testPath,
numPassingTests: result.results.numPassedTests,
numFailingTests: result.results.numFailedTests,
testResults: result.results.testResults,
duration: result.results.testResults[0]?.perfStats?.runtime || 0,
};
}
// ワーカープロセス初期化時の設定を保持
let workerConfig: WorkerOptions;
export function setup(options: WorkerOptions): void {
workerConfig = options;
}
このワーカーは、setup メソッドで初期化され、runTest メソッドでテストを実行します。jest-worker は、これらのメソッドを自動的に呼び出してくれるのです。
例 5:並列実行のパフォーマンス測定
並列実行の効果を測定するために、実行時間を記録する仕組みを追加できます。
typescript// performance-reporter.ts
import type { Reporter, TestResult } from '@jest/test-result';
import type { Config } from '@jest/types';
import * as fs from 'fs';
class PerformanceReporter implements Reporter {
private _startTime: number;
private _testTimings: Map<string, number>;
private _workerUtilization: Map<number, number>;
constructor(
globalConfig: Config.GlobalConfig,
options: any
) {
this._testTimings = new Map();
this._workerUtilization = new Map();
}
typescript // テストスイート開始時
onRunStart(): void {
this._startTime = Date.now();
console.log('テスト実行を開始します...');
}
// 各テスト完了時
onTestResult(
test: any,
testResult: TestResult,
results: any
): void {
const duration = testResult.perfStats.runtime;
this._testTimings.set(testResult.testFilePath, duration);
// ワーカー ID を記録(実際の実装では worker から取得)
const workerId = testResult.workerId || 0;
const current = this._workerUtilization.get(workerId) || 0;
this._workerUtilization.set(workerId, current + duration);
}
typescript // テストスイート完了時
onRunComplete(): void {
const totalTime = Date.now() - this._startTime;
// 統計情報を計算
const stats = this._calculateStats();
console.log('\n=== パフォーマンスレポート ===');
console.log(`総実行時間: ${totalTime}ms`);
console.log(`平均テスト時間: ${stats.averageTestTime}ms`);
console.log(`最長テスト時間: ${stats.maxTestTime}ms`);
console.log(`最短テスト時間: ${stats.minTestTime}ms`);
console.log('\n--- ワーカー使用率 ---');
typescript // ワーカーごとの使用率を表示
for (const [workerId, time] of this._workerUtilization) {
const utilization = (time / totalTime) * 100;
console.log(`ワーカー ${workerId}: ${utilization.toFixed(1)}%`);
}
// レポートをファイルに保存
this._saveReport({
totalTime,
stats,
workerUtilization: Array.from(this._workerUtilization.entries()),
});
}
typescript private _calculateStats() {
const timings = Array.from(this._testTimings.values());
return {
averageTestTime: timings.reduce((a, b) => a + b, 0) / timings.length,
maxTestTime: Math.max(...timings),
minTestTime: Math.min(...timings),
totalTests: timings.length,
};
}
private _saveReport(data: any): void {
fs.writeFileSync(
'jest-performance-report.json',
JSON.stringify(data, null, 2)
);
}
}
module.exports = PerformanceReporter;
このレポーターを使うと、各テストの実行時間やワーカーの使用率を可視化できます。パフォーマンスのボトルネックを特定するのに役立つでしょう。
Jest 設定ファイルに追加します。
typescript// jest.config.js
module.exports = {
maxWorkers: 4,
reporters: ['default', './performance-reporter.js'],
};
例 6:CI 環境での最適化
CI 環境では、リソースの制約や実行時間の要件に応じて、並列実行を最適化する必要があります。
typescript// jest.config.ci.js
const os = require('os');
// CI 環境を検出
const isCI = process.env.CI === 'true';
const cpuCount = os.cpus().length;
module.exports = {
// CI では CPU コア数 - 1 を使用
maxWorkers: isCI ? Math.max(cpuCount - 1, 2) : '50%',
// CI ではキャッシュを有効化
cache: true,
cacheDirectory: '.jest-cache',
// テストタイムアウトを短く設定
testTimeout: isCI ? 10000 : 30000,
typescript // CI では詳細なレポートを出力
verbose: isCI,
// カバレッジ収集(CI のみ)
collectCoverage: isCI,
coverageReporters: isCI ? ['json', 'lcov', 'text'] : ['text'],
// 失敗時は即座に停止(CI のみ)
bail: isCI ? 1 : 0,
};
このように、CI 環境とローカル環境で設定を使い分けることで、それぞれの環境に最適化されたテスト実行が可能になります。
次の表は、環境ごとの推奨設定をまとめたものです。
| # | 設定項目 | ローカル環境 | CI 環境 | 理由 |
|---|---|---|---|---|
| 1 | maxWorkers | 50% | CPU - 1 | ローカルでは他のアプリへの影響を最小化、CI では最大限活用 |
| 2 | testTimeout | 30000ms | 10000ms | CI では迅速なフィードバックを優先 |
| 3 | bail | 0(無効) | 1(有効) | CI では失敗時に即座に停止してリソースを節約 |
| 4 | cache | 任意 | 有効 | CI ではビルド間でキャッシュを共有して高速化 |
| 5 | verbose | false | true | CI では詳細なログを出力してデバッグを容易に |
この表から、環境ごとに最適な設定が異なることが分かります。ローカル環境では開発者の体験を優先し、CI 環境では速度とリソース効率を重視するのです。
図で理解する並列実行の全体フロー
最後に、Jest の並列実行の全体像を図でまとめてみましょう。
mermaidsequenceDiagram
participant CLI as Jest CLI
participant Scheduler as TestScheduler
participant Cache as TimingCache
participant Queue as TestQueue
participant W1 as ワーカー1
participant W2 as ワーカー2
participant Reporter as Reporter
CLI->>CLI: 設定を読み込む
CLI->>Scheduler: テスト実行を開始
Scheduler->>Cache: 過去の実行時間を取得
Cache-->>Scheduler: 実行時間データ
Scheduler->>Queue: テストをソートして<br/>キューに追加
Scheduler->>W1: test1.js 実行依頼
Scheduler->>W2: test2.js 実行依頼
Note over W1,W2: 並列実行中
W1->>W1: test1.js を実行
W2->>W2: test2.js を実行
W1-->>Scheduler: test1.js 結果<br/>(成功、2秒)
Scheduler->>Reporter: test1.js の結果を報告
Scheduler->>Cache: test1.js の実行時間を記録
Scheduler->>W1: test3.js 実行依頼
W2-->>Scheduler: test2.js 結果<br/>(成功、5秒)
Scheduler->>Reporter: test2.js の結果を報告
Scheduler->>Cache: test2.js の実行時間を記録
W1-->>Scheduler: test3.js 結果<br/>(成功、1秒)
Scheduler->>Reporter: test3.js の結果を報告
Scheduler->>Cache: test3.js の実行時間を記録
Scheduler->>Cache: キャッシュを保存
Scheduler->>Reporter: 全テスト完了
Reporter->>CLI: レポートを出力
この図は、Jest の並列実行の全体フローを時系列で示しています。設定の読み込みから、テストのソート、ワーカーへの割り当て、結果の集約まで、一連の流れが一目で理解できるでしょう。
特に注目すべきは、実行時間のキャッシングです。各テストの実行時間を記録し、次回のテスト実行時に最適な順序を決定することで、継続的にパフォーマンスを改善しているのです。
まとめ
Jest の並列実行は、Worker Pool パターンと動的タスク割り当てを組み合わせることで、高速かつ効率的なテスト実行を実現しています。
本記事で解説した主要なポイントを振り返りましょう。
Jest 並列実行の仕組み
-
Worker Pool パターン
- あらかじめワーカープロセスを起動しておき、タスクを効率的に割り当てる
- デフォルトでは CPU コア数に応じて最適なワーカー数を自動決定
-
動的タスク割り当て
- 空いたワーカーに次のテストを即座に割り当てる
- テスト実行時間のばらつきがあっても、全体の実行時間を最小化
-
プロセス間通信
- Node.js の
child_processモジュールを使った効率的な IPC - リクエスト ID による非同期通信の管理
- Node.js の
-
テストの独立性
- プロセス分離によるグローバル状態の隔離
- 各テストファイルごとの環境セットアップとクリーンアップ
-
実行順序の最適化
- 過去の実行時間データをキャッシュ
- 重いテストを優先的に実行することで待機時間を削減
パフォーマンスチューニングのポイント
Jest の並列実行を最大限活用するには、以下のポイントに注意しましょう。
| # | 項目 | 推奨事項 | 理由 |
|---|---|---|---|
| 1 | ワーカー数 | 環境に応じて調整 | ローカルは 50%、CI は CPU - 1 が目安 |
| 2 | テストの粒度 | ファイル単位で適切に分割 | 並列実行の恩恵を最大化 |
| 3 | 実行順序 | 重いテストを先に実行 | 待機時間を削減 |
| 4 | キャッシュ | 実行時間データを活用 | 継続的なパフォーマンス改善 |
| 5 | テストの独立性 | 共有状態を避ける | 並列実行の安定性を確保 |
実践的な活用方法
Jest の並列実行の仕組みを理解することで、以下のような実践的なメリットが得られます。
開発効率の向上
- テスト実行時間の大幅な短縮により、開発フィードバックサイクルが高速化します
- TDD(テスト駆動開発)がより実践しやすくなるでしょう
CI/CD パイプラインの最適化
- リソースを最大限活用した効率的なテスト実行が可能になります
- ビルド時間の短縮により、デプロイまでの時間を削減できるのです
カスタマイズの可能性
- 並列実行の仕組みを理解することで、独自のランナーやレポーターを実装できます
- プロジェクト固有の要件に応じた最適化が可能になるでしょう
さらなる学習のために
Jest の並列実行についてさらに深く学びたい方は、以下のトピックも探求してみてください。
- jest-worker の内部実装: Worker Pool パターンの詳細な実装を学べます
- テストのシャーディング: 複数の CI マシン間でテストを分散実行する方法です
- カスタム Test Sequencer: テスト実行順序を完全にカスタマイズする方法でしょう
- メモリ管理: 大規模なテストスイートでのメモリリークの検出と対策について理解できます
Jest の並列実行は、現代の Web 開発において不可欠な機能です。その仕組みを理解し、適切に活用することで、開発チーム全体の生産性を大きく向上させられるでしょう。
関連リンク
articleJest の並列実行はなぜ速い?実行キューとワーカーの舞台裏を読み解く
articleJest を可観測化する:JUnit/SARIF/OpenTelemetry で CI ダッシュボードを構築
articleJest でプロパティベーステスト:fast-check で仕様を壊れにくくする設計
articleJest expect.extend チートシート:実務で使えるカスタムマッチャー 40 連発
articleJest を Yarn PnP で動かす:ゼロ‐node_modules 時代の設定レシピ
articleJest の TS 変換速度を検証:ts-jest vs babel-jest vs swc-jest vs esbuild-jest
articlePrisma 読み書き分離設計:読み取りレプリカ/プロキシ/整合性モデルを整理
articleMermaid で日本語が潰れる問題を解決:フォント・エンコード・SVG 設定の勘所
articlePinia 2025 アップデート総まとめ:非互換ポイントと安全な移行チェックリスト
articleMCP サーバー 実装比較:Node.js/Python/Rust の速度・DX・コストをベンチマーク検証
articleLodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定
articleOllama のインストール完全ガイド:macOS/Linux/Windows(WSL)対応手順
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来