T-CREATOR

Jest のグローバルセットアップ/ティアダウンの活用法

Jest のグローバルセットアップ/ティアダウンの活用法

テストコードを書いていると、「なぜ毎回同じような初期化処理を書かなければいけないのか?」「テスト実行後のクリーンアップを忘れてしまう」といった悩みに直面することはありませんか?

そんな開発者の皆さんの悩みを解決するのが、Jest のグローバルセットアップ/ティアダウン機能です。この機能を活用すれば、テスト実行の前後で必要な処理を一箇所にまとめて管理でき、より効率的で保守性の高いテストコードが書けるようになります。

今回は、初心者の方でも理解しやすいように、実際のエラー例や具体的なコード例を交えながら、Jest のグローバルセットアップ/ティアダウンの基本的な活用法をご紹介いたします。

背景

テスト実行時の共通処理の必要性

現代の Web アプリケーション開発では、データベース接続、外部 API の認証、環境変数の設定など、テストを実行する前に多くの前準備が必要になります。

例えば、次のような処理が各テストファイルで重複してしまうことがよくあります:

typescript// user.test.ts
beforeAll(async () => {
  await connectDatabase();
  await setupTestData();
  process.env.NODE_ENV = 'test';
});

// product.test.ts
beforeAll(async () => {
  await connectDatabase();
  await setupTestData();
  process.env.NODE_ENV = 'test';
});

このように同じコードが複数のテストファイルに散らばっていると、保守性が低下し、変更時に多くのファイルを修正する必要が生じてしまいます。

テスト環境の一貫性確保の重要性

テストの信頼性を確保するためには、すべてのテストが同じ条件下で実行されることが極めて重要です。

環境の不整合により、以下のような問題が発生する可能性があります:

問題影響対策の必要性
データベース接続設定の違いテスト結果の不安定化
環境変数の設定漏れ予期しないテスト失敗
外部サービスの認証情報不備API テストの実行不可

課題

テストごとの重複した初期化処理

多くの開発者が直面する最初の課題は、テストファイルごとに同じような初期化処理を記述しなければならないことです。

例えば、以下のようなエラーに遭遇したことはありませんか?

typescript// テスト実行時によく見るエラー
FAIL  src/components/UserList.test.tsxTest suite failed to run

    TypeError: Cannot read property 'findMany' of undefined
      at Object.<anonymous> (src/components/UserList.test.tsx:15:21)

このエラーは、データベース接続の初期化が各テストファイルで適切に行われていないために発生します。

テスト後のクリーンアップ漏れ

テスト実行後のクリーンアップ処理を忘れてしまうと、以下のような問題が発生します:

typescript// よくあるクリーンアップ漏れの例
describe('User API', () => {
  beforeAll(async () => {
    await database.connect();
    await createTestUser();
  });

  // afterAll の処理を忘れがち!
  // afterAll(async () => {
  //   await cleanupTestData();
  //   await database.disconnect();
  // });
});

このようなクリーンアップ漏れにより、次のようなエラーが発生することがあります:

bashJest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests.

テスト環境の不整合

複数の開発者が同じプロジェクトで作業する場合、テスト環境の設定が人によって異なってしまうことがあります。これにより、「自分の環境では通るのに、他の人の環境では失敗する」という厄介な問題が生じます。

解決策

globalSetup の基本的な使い方

Jest の globalSetup は、すべてのテストスイートが実行される前に一度だけ実行される処理を定義できる機能です。

まず、プロジェクトのルートディレクトリに jest.config.js ファイルを作成しましょう:

javascript// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  globalSetup: './tests/setup/globalSetup.js',
  globalTeardown: './tests/setup/globalTeardown.js',
  setupFilesAfterEnv: ['./tests/setup/setupTests.js'],
};

次に、グローバルセットアップファイルを作成します:

javascript// tests/setup/globalSetup.js
const { execSync } = require('child_process');

module.exports = async () => {
  console.log('🚀 グローバルセットアップを開始します...');

  // 環境変数の設定
  process.env.NODE_ENV = 'test';
  process.env.DATABASE_URL =
    'postgresql://test:test@localhost:5432/testdb';

  console.log('✅ 環境変数を設定しました');
};

この設定により、すべてのテストが実行される前に必要な環境変数が自動的に設定されるようになります。

globalTeardown の実装方法

globalTeardown は、すべてのテストが完了した後に実行される処理を定義します。リソースのクリーンアップやデータベース接続の切断などに活用できます:

javascript// tests/setup/globalTeardown.js
module.exports = async () => {
  console.log('🧹 グローバルティアダウンを開始します...');

  // テストデータベースのクリーンアップ
  if (global.__DATABASE_CONNECTION__) {
    await global.__DATABASE_CONNECTION__.close();
    console.log('📚 データベース接続を切断しました');
  }

  // 一時ファイルの削除
  const fs = require('fs');
  const path = require('path');
  const tempDir = path.join(__dirname, '../../temp');

  if (fs.existsSync(tempDir)) {
    fs.rmSync(tempDir, { recursive: true, force: true });
    console.log('🗑️ 一時ファイルを削除しました');
  }

  console.log('✨ クリーンアップが完了しました');
};

Jest 設定ファイルの構成

TypeScript プロジェクトでより高度な設定を行う場合は、以下のような jest.config.ts を作成することをお勧めします:

typescript// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: [
    '**/__tests__/**/*.+(ts|tsx|js)',
    '**/*.(test|spec).+(ts|tsx|js)',
  ],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
  globalSetup: '<rootDir>/tests/setup/globalSetup.ts',
  globalTeardown: '<rootDir>/tests/setup/globalTeardown.ts',
  setupFilesAfterEnv: [
    '<rootDir>/tests/setup/setupTests.ts',
  ],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
  ],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

export default config;

この設定では、TypeScript の型安全性を活用しながら、パスエイリアスやカバレッジ収集の設定も含めています。

具体例

シンプルなセットアップ/ティアダウンの実装

実際の開発現場でよく使われる、シンプルで実用的な例をご紹介します。

まず、データベース接続を管理するグローバルセットアップを作成しましょう:

typescript// tests/setup/globalSetup.ts
import { Pool } from 'pg';

declare global {
  var __DATABASE_POOL__: Pool;
}

export default async function globalSetup() {
  console.log('🔧 テスト環境を準備中...');

  // データベース接続プールの作成
  const pool = new Pool({
    host: 'localhost',
    port: 5432,
    database: 'test_db',
    user: 'test_user',
    password: 'test_password',
    max: 5, // 最大接続数
  });

  try {
    // 接続テスト
    const client = await pool.connect();
    await client.query('SELECT NOW()');
    client.release();

    console.log('✅ データベース接続が確立されました');

    // グローバル変数に保存
    global.__DATABASE_POOL__ = pool;
  } catch (error) {
    console.error(
      '❌ データベース接続に失敗しました:',
      error
    );
    throw error;
  }
}

対応するティアダウン処理は以下のようになります:

typescript// tests/setup/globalTeardown.ts
export default async function globalTeardown() {
  console.log('🧽 テスト環境をクリーンアップ中...');

  if (global.__DATABASE_POOL__) {
    try {
      await global.__DATABASE_POOL__.end();
      console.log('✅ データベース接続プールを閉じました');
    } catch (error) {
      console.error(
        '❌ プール終了時にエラーが発生:',
        error
      );
    }
  }

  console.log('🎉 クリーンアップが完了しました');
}

基本的な設定例

Next.js プロジェクトでの実践的な設定例をご紹介します:

typescript// tests/setup/setupTests.ts
import '@testing-library/jest-dom';

// カスタムマッチャーの設定
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeValidEmail(): R;
    }
  }
}

// メールアドレスの検証用カスタムマッチャー
expect.extend({
  toBeValidEmail(received: string) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);

    if (pass) {
      return {
        message: () =>
          `期待値: ${received} は有効なメールアドレスではありません`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `期待値: ${received} は有効なメールアドレスです`,
        pass: false,
      };
    }
  },
});

環境変数を管理するための設定も追加しましょう:

typescript// tests/setup/env.ts
export function setupTestEnvironment() {
  // 必要な環境変数を設定
  const requiredEnvVars = {
    NODE_ENV: 'test',
    NEXT_PUBLIC_API_URL: 'http://localhost:3001',
    DATABASE_URL:
      'postgresql://test:test@localhost:5432/testdb',
    JWT_SECRET: 'test-jwt-secret-key',
  };

  Object.entries(requiredEnvVars).forEach(
    ([key, value]) => {
      if (!process.env[key]) {
        process.env[key] = value;
        console.log(`🔧 環境変数 ${key} を設定しました`);
      }
    }
  );
}

// グローバルセットアップから呼び出し
export { setupTestEnvironment };

これらの設定により、テストの実行が格段に安定し、開発効率が向上することを実感していただけるでしょう。

まとめ

Jest のグローバルセットアップ/ティアダウン機能は、一見複雑に思えるかもしれませんが、一度理解するとテスト開発の生産性を劇的に向上させる強力なツールです。

この記事でご紹介した内容をまとめると:

項目メリット実装のポイント
globalSetup重複する初期化処理の排除環境変数・DB 接続の一元管理
globalTeardown確実なリソース解放接続切断・ファイル削除の自動化
設定ファイルの整備チーム全体での環境統一TypeScript 対応・パスエイリアス

特に重要なのは、**「テストが失敗した時に、環境の問題なのかコードの問題なのかがすぐに判断できる」**ということです。グローバルセットアップ/ティアダウンにより、テスト環境が常に一定の状態に保たれるため、純粋にコードの品質に集中できるようになります。

最初は設定が面倒に感じるかもしれませんが、長期的な開発効率とコードの品質向上を考えると、必ず投資する価値があります。まずは小さなプロジェクトから始めて、徐々に活用範囲を広げていくことをお勧めいたします。

皆さんのテスト開発がより楽しく、効率的になることを心から願っています!

関連リンク