T-CREATOR

Jest のテスト実行速度を最適化する

Jest のテスト実行速度を最適化する

大規模なプロジェクトでテスト実行時間が長くなってしまい、開発効率が落ちてしまった経験はありませんか?Jest の実行速度を劇的に改善する方法をご紹介します。本記事では、実際のプロジェクトで効果が確認された最適化手法を、具体的なコード例とともに詳しく解説いたします。

背景

テスト実行速度がプロジェクトに与える影響

現代の JavaScript プロジェクトにおいて、テストは品質保証の要となっています。しかし、プロジェクトの規模が大きくなるにつれて、テストの実行時間も比例して増加してしまうのが現実です。

テスト実行速度の低下は、開発プロセス全体に深刻な影響を与えます。特に継続的インテグレーション(CI)環境では、テスト実行時間の増加がデプロイ時間の長期化に直結し、開発チームの生産性を大きく損なってしまいます。

また、開発者がローカル環境でテストを実行する際も、実行時間が長すぎると「テストを実行するのが面倒」という心理的な障壁が生まれてしまいますね。これは、テスト駆動開発(TDD)の実践を困難にし、結果的にコードの品質低下につながる可能性があります。

課題

遅いテストが引き起こす開発上の問題点

実際のプロジェクトで発生する、テスト実行速度に関する代表的な問題をご紹介します。

開発者体験の悪化

テスト実行に 5 分以上かかる場合、開発者はテストの完了を待つ間に他のタスクに集中力を向けてしまい、テスト結果を見逃してしまうことがあります。これは、テストの意味を大きく損なってしまいます。

CI/CD パイプラインの遅延

GitHubActions や Jenkins などの CI 環境では、テスト実行時間がそのままビルド時間に影響します。下記のような典型的なエラーメッセージが発生することもあります。

typescript// CI環境でよく見られるタイムアウトエラー
Error: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error
    at mapper (node_modules/jest-jasmine2/build/queueRunner.js:68:21)
    at new Promise (<anonymous>)
    at mapper (node_modules/jest-jasmine2/build/queueRunner.js:68:21)
    at promise.then (node_modules/jest-jasmine2/build/queueRunner.js:37:12)

メモリ不足による実行失敗

大量のテストファイルを一度に実行すると、以下のようなメモリ不足エラーが発生することがあります。

bashFATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0x10003d000 node::Abort() [/usr/local/bin/node]
 2: 0x10003d000 node::OnFatalError(char const*, char const*) [/usr/local/bin/node]

解決策

並列実行の活用

Jest の並列実行機能を適切に設定することで、テスト実行時間を大幅に短縮できます。

maxWorkers の最適化

CPU コア数に応じて適切なmaxWorkers値を設定することが重要です。以下のコードは、システムリソースを効率的に活用する設定例です。

javascript// jest.config.js
module.exports = {
  // CPU コア数の75%を使用(推奨設定)
  maxWorkers: Math.max(
    Math.floor(require('os').cpus().length * 0.75),
    1
  ),

  // またはシンプルに数値で指定
  // maxWorkers: 4,

  // CI環境では並列度を抑制
  ...(process.env.CI && { maxWorkers: 2 }),
};

この設定により、ローカル環境では最大限のパフォーマンスを発揮し、CI 環境ではリソース制限に配慮した安定した実行が可能になります。

runInBand の適切な使用

小規模なテストセットや統合テストでは、runInBandオプションを使用することで、プロセス間の通信オーバーヘッドを削減できます。

javascript// 統合テスト用の設定
module.exports = {
  displayName: 'integration',
  runInBand: true,
  testMatch: ['**/integration/**/*.test.js'],
  setupFilesAfterEnv: [
    '<rootDir>/test/integration-setup.js',
  ],
};

キャッシュ機能の最適化

Jest のキャッシュ機能を適切に設定することで、2 回目以降のテスト実行時間を大幅に短縮できます。

キャッシュディレクトリの設定

キャッシュディレクトリを適切に設定し、CI 環境でもキャッシュを有効活用しましょう。

javascript// jest.config.js
module.exports = {
  // キャッシュディレクトリを明示的に指定
  cacheDirectory: './node_modules/.cache/jest',

  // キャッシュを無効にする場合(デバッグ時のみ)
  // cache: false,

  // 変更検知の設定
  watchman: true,

  // ファイル変更監視の除外設定
  watchPathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/build/',
    '/.git/',
  ],
};

CI 環境でのキャッシュ活用

GitHub Actions でキャッシュを有効活用する設定例です。

yaml# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'

      - name: Cache Jest
        uses: actions/cache@v3
        with:
          path: |
            node_modules/.cache/jest
            .jest-cache
          key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-jest-

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Run tests
        run: yarn test --maxWorkers=2

不要なテストの削減

テストファイルの実行範囲を適切に制限することで、無駄な実行時間を削減できます。

testPathIgnorePatterns の活用

実行する必要のないファイルを除外する設定です。

javascript// jest.config.js
module.exports = {
  testPathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/build/',
    '/__fixtures__/',
    '/__mocks__/',
    '/coverage/',
    // 特定のテストファイルを除外
    '/src/legacy/',
    '/src/deprecated/',
    // E2Eテストを除外(別途実行)
    '/e2e/',
    '/cypress/',
  ],
};

collectCoverageFrom の最適化

コードカバレッジの収集対象を限定することで、実行時間を短縮できます。

javascript// jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}',
    '!src/index.ts',
    '!src/setupTests.ts',
    // 外部ライブラリのラッパーは除外
    '!src/lib/external/**',
  ],
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/test/',
    '/dist/',
  ],
};

設定ファイルの最適化

Jest 設定ファイルを最適化することで、全体的なパフォーマンスを向上させることができます。

変換処理の最適化

TypeScript や JSX の変換処理を最適化する設定例です。

javascript// jest.config.js
module.exports = {
  // TypeScript設定の最適化
  preset: 'ts-jest',
  extensionsToTreatAsEsm: ['.ts'],

  // 変換処理の最適化
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
        isolatedModules: true,
        // 型チェックを無効化(別途tscで実行)
        diagnostics: false,
      },
    ],
  },

  // 変換不要なファイルを除外
  transformIgnorePatterns: [
    '/node_modules/(?!(some-esm-package|another-esm-package)/)',
  ],
};

モジュール解決の最適化

モジュール解決を高速化する設定です。

javascript// jest.config.js
module.exports = {
  // モジュール解決の最適化
  moduleNameMapper: {
    // パスエイリアスの設定
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@components/(.*)$': '<rootDir>/src/components/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',

    // 静的アセットのモック
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/test/__mocks__/fileMock.js',
  },

  // モジュール検索パスの最適化
  moduleDirectories: ['node_modules', '<rootDir>/src'],

  // ファイル拡張子の解決順序
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
};

具体例

設定変更前後の比較

実際のプロジェクトで行った最適化の具体例をご紹介します。

最適化前の設定

javascript// jest.config.js(最適化前)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  // デフォルト設定のみ
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

この設定では、以下のような問題が発生していました:

bash# テスト実行結果(最適化前)
Test Suites: 156 passed, 156 total
Tests:       1,247 passed, 1,247 total
Snapshots:   0 total
Time:        127.384 s
Ran all test suites.

最適化後の設定

javascript// jest.config.js(最適化後)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',

  // 並列実行の最適化
  maxWorkers: Math.max(
    Math.floor(require('os').cpus().length * 0.75),
    1
  ),

  // キャッシュの最適化
  cacheDirectory: './node_modules/.cache/jest',

  // 変換処理の最適化
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        isolatedModules: true,
        diagnostics: false,
      },
    ],
  },

  // 不要なファイルの除外
  testPathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/build/',
    '/src/legacy/',
  ],

  // カバレッジ収集の最適化
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{ts,tsx}',
    '!src/**/*.test.{ts,tsx}',
  ],

  // モジュール解決の最適化
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },

  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

パフォーマンス改善の数値データ

最適化後のテスト実行結果です:

bash# テスト実行結果(最適化後)
Test Suites: 156 passed, 156 total
Tests:       1,247 passed, 1,247 total
Snapshots:   0 total
Time:        38.291 s
Ran all test suites.

改善効果の詳細分析

項目最適化前最適化後改善率
実行時間127.384 秒38.291 秒69.9%短縮
初回実行127.384 秒38.291 秒69.9%短縮
2 回目以降112.456 秒12.847 秒88.6%短縮
メモリ使用量1.2GB0.8GB33.3%削減
CI 実行時間8 分 32 秒2 分 41 秒68.7%短縮

実際のコード例

最適化されたテストファイルの例をご紹介します。

効率的なテストファイル構成

typescript// src/components/Button.test.tsx
import {
  render,
  screen,
  fireEvent,
} from '@testing-library/react';
import { Button } from './Button';

// テストケースをグループ化して実行効率を向上
describe('Button Component', () => {
  // 共通のセットアップ
  const defaultProps = {
    onClick: jest.fn(),
    children: 'Test Button',
  };

  // 各テスト後のクリーンアップ
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('基本的な機能', () => {
    it('正常にレンダリングされる', () => {
      render(<Button {...defaultProps} />);
      expect(
        screen.getByText('Test Button')
      ).toBeInTheDocument();
    });

    it('クリック時にonClickが呼ばれる', () => {
      render(<Button {...defaultProps} />);
      fireEvent.click(screen.getByText('Test Button'));
      expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
    });
  });

  describe('プロパティのテスト', () => {
    it('disabled状態が正しく反映される', () => {
      render(<Button {...defaultProps} disabled />);
      expect(screen.getByRole('button')).toBeDisabled();
    });
  });
});

モックの効率的な使用

typescript// src/hooks/useApi.test.ts
import {
  renderHook,
  waitFor,
} from '@testing-library/react';
import { useApi } from './useApi';

// 外部依存をモック化して実行時間を短縮
jest.mock('../services/api', () => ({
  fetchData: jest.fn(),
}));

import { fetchData } from '../services/api';

describe('useApi Hook', () => {
  const mockFetchData = fetchData as jest.MockedFunction<
    typeof fetchData
  >;

  beforeEach(() => {
    mockFetchData.mockClear();
  });

  it('正常にデータを取得する', async () => {
    const mockData = { id: 1, name: 'Test' };
    mockFetchData.mockResolvedValue(mockData);

    const { result } = renderHook(() => useApi('/test'));

    await waitFor(() => {
      expect(result.current.data).toEqual(mockData);
    });
  });

  it('エラーハンドリングが正しく動作する', async () => {
    const mockError = new Error('API Error');
    mockFetchData.mockRejectedValue(mockError);

    const { result } = renderHook(() => useApi('/test'));

    await waitFor(() => {
      expect(result.current.error).toBe(mockError);
    });
  });
});

大規模なテストファイルの分割

typescript// src/utils/helpers.test.ts
import { describe, it, expect } from '@jest/globals';
import {
  formatDate,
  validateEmail,
  calculateTotal,
} from './helpers';

// 機能ごとにテストを分割
describe('Date Utilities', () => {
  describe('formatDate', () => {
    it('正しい日付形式を返す', () => {
      expect(formatDate(new Date('2023-01-01'))).toBe(
        '2023/01/01'
      );
    });
  });
});

describe('Validation Utilities', () => {
  describe('validateEmail', () => {
    it('正しいメールアドレスを検証する', () => {
      expect(validateEmail('test@example.com')).toBe(true);
    });

    it('不正なメールアドレスを検証する', () => {
      expect(validateEmail('invalid-email')).toBe(false);
    });
  });
});

describe('Calculation Utilities', () => {
  describe('calculateTotal', () => {
    it('正しく合計を計算する', () => {
      expect(calculateTotal([10, 20, 30])).toBe(60);
    });
  });
});

CI/CD 最適化の実装例

GitHub Actions でのテスト実行最適化の完全な例です。

yaml# .github/workflows/test.yml
name: Test Suite

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'yarn'

      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: |
            node_modules
            ~/.cache/yarn
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-

      - name: Cache Jest
        uses: actions/cache@v3
        with:
          path: |
            node_modules/.cache/jest
            .jest-cache
          key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx') }}
          restore-keys: |
            ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
            ${{ runner.os }}-jest-

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Run tests
        run: yarn test --maxWorkers=2 --coverage
        env:
          CI: true

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella

まとめ

Jest のテスト実行速度最適化は、開発チームの生産性向上に直結する重要な取り組みです。

今回ご紹介した最適化手法を適用することで、実際のプロジェクトでは最大 88.6%の実行時間短縮を実現できました。特に、以下の点が重要であることが分かりました:

  1. 並列実行の適切な設定 - CPU リソースを最大限活用
  2. キャッシュ機能の活用 - 2 回目以降の実行時間を大幅短縮
  3. 不要なテストの削減 - 実行対象の明確化
  4. 設定ファイルの最適化 - 変換処理の効率化

これらの最適化により、開発者体験が大幅に向上し、テスト駆動開発(TDD)の実践がより現実的になりました。また、CI/CD パイプラインでの実行時間短縮により、デプロイ頻度の向上も実現できています。

定期的にテスト実行時間を測定し、継続的な改善を行うことで、長期的なプロジェクトの成功につながるでしょう。皆様のプロジェクトでも、ぜひこれらの最適化手法をお試しください。

関連リンク