T-CREATOR

Jest beforeEach・afterEach の正しい使い方

Jest beforeEach・afterEach の正しい使い方

効果的なテストを書く上で、テスト実行前後の適切なセットアップとクリーンアップは欠かせません。Jest では、beforeEach・afterEach を使ってテストライフサイクルを効率的に管理できます。しかし、これらの機能を正しく理解せずに使用すると、テストが不安定になったり、予期しない副作用が発生したりする可能性があります。

この記事では、Jest の beforeEach・afterEach の正しい使い方を詳しく解説いたします。基本概念から実践的な活用方法、パフォーマンス最適化まで、体系的に学習して確実にマスターできるようになりましょう。

beforeEach・afterEach の基本概念

テストライフサイクルフックとは

beforeEach・afterEach は、Jest が提供するテストライフサイクルフック(ライフサイクルメソッド)です。これらを使用することで、各テストの実行前後に共通の処理を自動実行できます。

4 つのライフサイクルフック

javascriptdescribe('テストライフサイクルの例', () => {
  beforeAll(() => {
    console.log('テストスイート開始前に1回実行');
  });

  afterAll(() => {
    console.log('テストスイート終了後に1回実行');
  });

  beforeEach(() => {
    console.log('各テスト実行前に毎回実行');
  });

  afterEach(() => {
    console.log('各テスト実行後に毎回実行');
  });

  test('テスト1', () => {
    expect(1 + 1).toBe(2);
  });

  test('テスト2', () => {
    expect(2 + 2).toBe(4);
  });
});

beforeEach・afterEach の役割

beforeEach の主な用途

#用途
1テストデータ準備ダミーデータやモックの初期化
2環境初期化DOM 要素の作成、変数の初期化
3外部リソース接続データベース接続、API クライアント
4モック・スパイ設定関数のモック化、監視設定

afterEach の主な用途

javascriptdescribe('ユーザー管理テスト', () => {
  let mockDatabase;
  let testUser;

  beforeEach(() => {
    // テスト用データベース接続
    mockDatabase = createMockDatabase();

    // テスト用ユーザーデータ
    testUser = {
      id: 1,
      name: 'テストユーザー',
      email: 'test@example.com',
    };

    // モック関数の初期化
    jest.clearAllMocks();
  });

  afterEach(() => {
    // データベース接続のクリーンアップ
    mockDatabase.disconnect();

    // テストデータのリセット
    testUser = null;

    // DOM要素のクリーンアップ
    document.body.innerHTML = '';
  });

  test('ユーザーを作成できる', () => {
    const result = createUser(testUser);
    expect(result).toBeDefined();
  });

  test('ユーザーを削除できる', () => {
    const result = deleteUser(testUser.id);
    expect(result).toBe(true);
  });
});

セットアップとクリーンアップの重要性

テスト間の独立性確保

各テストは他のテストの影響を受けることなく、独立して実行できる必要があります。beforeEach・afterEach を適切に使用することで、この原則を守れます。

javascriptdescribe('カウンター機能', () => {
  let counter;

  beforeEach(() => {
    // 各テストで新しいカウンターインスタンスを作成
    counter = new Counter();
  });

  test('初期値は0である', () => {
    expect(counter.getValue()).toBe(0);
  });

  test('インクリメントで値が増える', () => {
    counter.increment();
    expect(counter.getValue()).toBe(1);
  });

  test('複数回インクリメントできる', () => {
    counter.increment();
    counter.increment();
    counter.increment();
    expect(counter.getValue()).toBe(3);
  });
  // 各テストが独立して動作する
});

テストライフサイクルの理解

実行順序の詳細

Jest のテストライフサイクルフックは、特定の順序で実行されます。この順序を理解することは、適切なテスト設計において重要です。

基本的な実行順序

javascriptdescribe('実行順序の確認', () => {
  beforeAll(() => {
    console.log('1. beforeAll実行');
  });

  beforeEach(() => {
    console.log('2. beforeEach実行');
  });

  afterEach(() => {
    console.log('4. afterEach実行');
  });

  afterAll(() => {
    console.log('5. afterAll実行');
  });

  test('テストA', () => {
    console.log('3. テストA実行');
    expect(true).toBe(true);
  });

  test('テストB', () => {
    console.log('3. テストB実行');
    expect(true).toBe(true);
  });
});

// 出力結果:
// 1. beforeAll実行
// 2. beforeEach実行
// 3. テストA実行
// 4. afterEach実行
// 2. beforeEach実行
// 3. テストB実行
// 4. afterEach実行
// 5. afterAll実行

複数のフックがある場合の動作

同じ種類のフックを複数定義した場合の動作も理解しておきましょう。

javascriptdescribe('複数フックのテスト', () => {
  beforeEach(() => {
    console.log('beforeEach 1');
  });

  beforeEach(() => {
    console.log('beforeEach 2');
  });

  afterEach(() => {
    console.log('afterEach 1');
  });

  afterEach(() => {
    console.log('afterEach 2');
  });

  test('サンプルテスト', () => {
    console.log('テスト実行');
    expect(1).toBe(1);
  });
});

// 実行順序:
// beforeEach 1
// beforeEach 2
// テスト実行
// afterEach 1
// afterEach 2

実際のテストケースでの活用例

javascriptdescribe('ユーザー認証システム', () => {
  let authService;
  let mockLogger;

  beforeEach(() => {
    // 認証サービスの初期化
    authService = new AuthService();
    console.log('認証サービス初期化完了');
  });

  beforeEach(() => {
    // ログ機能のモック化
    mockLogger = {
      info: jest.fn(),
      error: jest.fn(),
    };
    authService.setLogger(mockLogger);
    console.log('ログ機能設定完了');
  });

  afterEach(() => {
    // セッションのクリーンアップ
    authService.clearSession();
    console.log('セッションクリア完了');
  });

  afterEach(() => {
    // モックのリセット
    jest.clearAllMocks();
    console.log('モックリセット完了');
  });

  test('正常ログインができる', async () => {
    const credentials = {
      username: 'testuser',
      password: 'password123',
    };

    const result = await authService.login(credentials);

    expect(result.success).toBe(true);
    expect(mockLogger.info).toHaveBeenCalledWith(
      'ログイン成功'
    );
  });
});

基本的なセットアップとクリーンアップ

React コンポーネントテストでの活用

React コンポーネントのテストでは、レンダリング前後の処理が重要になります。

javascriptimport React from 'react';
import { render, cleanup } from '@testing-library/react';
import UserProfile from './UserProfile';

describe('UserProfile コンポーネント', () => {
  let mockUser;
  let mockOnEdit;
  let mockOnDelete;

  beforeEach(() => {
    // テストデータの準備
    mockUser = {
      id: 1,
      name: '田中太郎',
      email: 'tanaka@example.com',
      role: 'admin',
    };

    // モック関数の作成
    mockOnEdit = jest.fn();
    mockOnDelete = jest.fn();

    // グローバル変数の初期化
    window.localStorage.clear();
    window.sessionStorage.clear();
  });

  afterEach(() => {
    // DOM のクリーンアップ
    cleanup();

    // モック関数のリセット
    jest.resetAllMocks();

    // タイマーのクリーンアップ
    jest.clearAllTimers();
  });

  test('ユーザー情報が正しく表示される', () => {
    const { getByText } = render(
      <UserProfile
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    expect(getByText('田中太郎')).toBeInTheDocument();
    expect(
      getByText('tanaka@example.com')
    ).toBeInTheDocument();
  });

  test('編集ボタンクリックで関数が呼ばれる', () => {
    const { getByText } = render(
      <UserProfile
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    const editButton = getByText('編集');
    editButton.click();

    expect(mockOnEdit).toHaveBeenCalledWith(mockUser.id);
  });
});

データベース操作のテスト

データベースを使用するテストでは、トランザクション管理が重要です。

javascriptimport { DatabaseConnection } from './database';
import { UserRepository } from './UserRepository';

describe('UserRepository データベーステスト', () => {
  let connection;
  let userRepository;
  let transaction;

  beforeEach(async () => {
    // データベース接続の確立
    connection = await DatabaseConnection.connect({
      database: 'test_db',
      host: 'localhost',
      port: 5432,
    });

    // トランザクション開始
    transaction = await connection.beginTransaction();

    // リポジトリのセットアップ
    userRepository = new UserRepository(connection);
  });

  afterEach(async () => {
    // トランザクションのロールバック
    if (transaction) {
      await transaction.rollback();
    }

    // 接続のクローズ
    if (connection) {
      await connection.close();
    }
  });

  test('新しいユーザーを作成できる', async () => {
    const userData = {
      name: '山田花子',
      email: 'yamada@example.com',
      password: 'securepassword',
    };

    const createdUser = await userRepository.create(
      userData
    );

    expect(createdUser.id).toBeDefined();
    expect(createdUser.name).toBe(userData.name);
    expect(createdUser.email).toBe(userData.email);
  });

  test('ユーザーを検索できる', async () => {
    // テストデータの挿入
    const testUser = await userRepository.create({
      name: '検索テスト',
      email: 'search@example.com',
      password: 'password',
    });

    const foundUser = await userRepository.findByEmail(
      'search@example.com'
    );

    expect(foundUser).toBeDefined();
    expect(foundUser.name).toBe('検索テスト');
  });
});

ファイルシステム操作のクリーンアップ

ファイル操作を含むテストでは、テスト用ディレクトリの管理が必要です。

javascriptimport fs from 'fs/promises';
import path from 'path';
import { FileManager } from './FileManager';

describe('FileManager ファイル操作テスト', () => {
  let testDirectory;
  let fileManager;

  beforeEach(async () => {
    // テスト用ディレクトリの作成
    testDirectory = path.join(__dirname, 'test-files');
    await fs.mkdir(testDirectory, { recursive: true });

    // FileManager インスタンスの作成
    fileManager = new FileManager(testDirectory);
  });

  afterEach(async () => {
    // テストファイルの削除
    try {
      await fs.rmdir(testDirectory, { recursive: true });
    } catch (error) {
      // ディレクトリが存在しない場合は無視
      if (error.code !== 'ENOENT') {
        console.warn(
          'テストディレクトリの削除に失敗:',
          error
        );
      }
    }
  });

  test('ファイルを作成できる', async () => {
    const fileName = 'test.txt';
    const content = 'テストファイルの内容';

    await fileManager.createFile(fileName, content);

    const filePath = path.join(testDirectory, fileName);
    const savedContent = await fs.readFile(
      filePath,
      'utf-8'
    );
    expect(savedContent).toBe(content);
  });

  test('ファイル一覧を取得できる', async () => {
    // 複数のテストファイルを作成
    await fileManager.createFile('file1.txt', 'content1');
    await fileManager.createFile('file2.txt', 'content2');

    const files = await fileManager.listFiles();

    expect(files).toHaveLength(2);
    expect(files).toContain('file1.txt');
    expect(files).toContain('file2.txt');
  });
});

非同期処理での beforeEach・afterEach 活用

非同期セットアップの基本

非同期処理を含むセットアップでは、async/await を使用して適切に処理を待つ必要があります。

javascriptdescribe('非同期 API テスト', () => {
  let apiClient;
  let mockServer;

  beforeEach(async () => {
    // モックサーバーの起動(非同期)
    mockServer = await startMockServer({
      port: 3001,
      routes: {
        '/api/users': [
          { id: 1, name: 'ユーザー1' },
          { id: 2, name: 'ユーザー2' },
        ],
      },
    });

    // API クライアントの初期化
    apiClient = new ApiClient({
      baseURL: 'http://localhost:3001',
      timeout: 5000,
    });

    // 接続確認
    await apiClient.healthCheck();
  });

  afterEach(async () => {
    // API クライアントの切断
    if (apiClient) {
      await apiClient.disconnect();
    }

    // モックサーバーの停止
    if (mockServer) {
      await mockServer.stop();
    }
  });

  test('ユーザー一覧を取得できる', async () => {
    const users = await apiClient.getUsers();

    expect(users).toHaveLength(2);
    expect(users[0].name).toBe('ユーザー1');
  });

  test('特定ユーザーを取得できる', async () => {
    const user = await apiClient.getUser(1);

    expect(user.id).toBe(1);
    expect(user.name).toBe('ユーザー1');
  });
});

Promise とエラーハンドリング

非同期処理では、適切なエラーハンドリングも重要です。

javascriptdescribe('Redis キャッシュテスト', () => {
  let redisClient;
  let cacheService;

  beforeEach(async () => {
    try {
      // Redis クライアントの接続
      redisClient = await createRedisClient({
        host: 'localhost',
        port: 6379,
        retryAttempts: 3,
      });

      // キャッシュサービスの初期化
      cacheService = new CacheService(redisClient);

      // 既存データのクリア
      await redisClient.flushdb();
    } catch (error) {
      console.error('Redis 接続に失敗しました:', error);
      throw error;
    }
  });

  afterEach(async () => {
    try {
      // キャッシュデータのクリーンアップ
      if (redisClient) {
        await redisClient.flushdb();
        await redisClient.quit();
      }
    } catch (error) {
      console.warn('Redis 切断時のエラー:', error);
      // テストを継続するため、エラーは投げない
    }
  });

  test('データをキャッシュできる', async () => {
    const key = 'test-key';
    const value = { message: 'テストデータ' };

    await cacheService.set(key, value, 60);
    const cachedValue = await cacheService.get(key);

    expect(cachedValue).toEqual(value);
  });

  test('期限切れデータは取得できない', async () => {
    const key = 'expire-test';
    const value = { temp: 'data' };

    // 1秒で期限切れ
    await cacheService.set(key, value, 1);

    // 2秒待機
    await new Promise((resolve) =>
      setTimeout(resolve, 2000)
    );

    const result = await cacheService.get(key);
    expect(result).toBeNull();
  });
});

タイムアウト処理の管理

長時間の非同期処理では、タイムアウト設定も考慮する必要があります。

javascriptdescribe('大容量データ処理テスト', () => {
  let dataProcessor;
  let testDatabase;

  // テスト全体のタイムアウトを延長
  jest.setTimeout(30000);

  beforeEach(async () => {
    // データベース接続(最大10秒)
    testDatabase = await connectToDatabase({
      timeout: 10000,
    });

    // 大容量テストデータの作成
    dataProcessor = new DataProcessor({
      batchSize: 1000,
      maxConcurrency: 5,
    });
  }, 15000); // beforeEach のタイムアウトを15秒に設定

  afterEach(async () => {
    // 処理中のジョブを停止
    if (dataProcessor) {
      await dataProcessor.cancelAllJobs();
    }

    // データベースのクリーンアップ
    if (testDatabase) {
      await testDatabase.truncateAllTables();
      await testDatabase.disconnect();
    }
  }, 10000); // afterEach のタイムアウトを10秒に設定

  test('大量データを並列処理できる', async () => {
    const testData = generateTestData(10000);

    const result = await dataProcessor.processBatch(
      testData
    );

    expect(result.processed).toBe(10000);
    expect(result.errors).toBe(0);
  });
});

ネストした describe での適用範囲

階層構造でのフック実行順序

ネストした describe ブロックでは、フックの実行順序を正しく理解することが重要です。

javascriptdescribe('親 describe', () => {
  beforeEach(() => {
    console.log('親 beforeEach');
  });

  afterEach(() => {
    console.log('親 afterEach');
  });

  test('親レベルのテスト', () => {
    console.log('親テスト実行');
    expect(true).toBe(true);
  });

  describe('子 describe', () => {
    beforeEach(() => {
      console.log('子 beforeEach');
    });

    afterEach(() => {
      console.log('子 afterEach');
    });

    test('子レベルのテスト', () => {
      console.log('子テスト実行');
      expect(true).toBe(true);
    });

    describe('孫 describe', () => {
      beforeEach(() => {
        console.log('孫 beforeEach');
      });

      afterEach(() => {
        console.log('孫 afterEach');
      });

      test('孫レベルのテスト', () => {
        console.log('孫テスト実行');
        expect(true).toBe(true);
      });
    });
  });
});

// 実行順序(孫テストの場合):
// 親 beforeEach
// 子 beforeEach
// 孫 beforeEach
// 孫テスト実行
// 孫 afterEach
// 子 afterEach
// 親 afterEach

段階的なセットアップパターン

階層構造を活用して、段階的にテスト環境を構築できます。

javascriptdescribe('ECサイト テストスイート', () => {
  let database;
  let app;

  beforeEach(async () => {
    // 基本的なデータベース接続
    database = await connectToTestDatabase();
  });

  afterEach(async () => {
    await database.disconnect();
  });

  describe('ユーザー管理機能', () => {
    let userService;

    beforeEach(() => {
      // ユーザーサービスの初期化
      userService = new UserService(database);
    });

    test('ユーザー登録ができる', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'password123',
      };

      const user = await userService.register(userData);
      expect(user.id).toBeDefined();
    });

    describe('認証機能', () => {
      let authService;
      let testUser;

      beforeEach(async () => {
        // 認証サービスの初期化
        authService = new AuthService(userService);

        // テスト用ユーザーの作成
        testUser = await userService.register({
          email: 'auth@example.com',
          password: 'testpass',
        });
      });

      test('正しい認証情報でログインできる', async () => {
        const result = await authService.login({
          email: 'auth@example.com',
          password: 'testpass',
        });

        expect(result.success).toBe(true);
        expect(result.token).toBeDefined();
      });

      test('間違った認証情報ではログインできない', async () => {
        const result = await authService.login({
          email: 'auth@example.com',
          password: 'wrongpass',
        });

        expect(result.success).toBe(false);
        expect(result.error).toBe('認証に失敗しました');
      });
    });
  });

  describe('商品管理機能', () => {
    let productService;

    beforeEach(() => {
      // 商品サービスの初期化
      productService = new ProductService(database);
    });

    describe('在庫管理', () => {
      let inventoryService;
      let testProducts;

      beforeEach(async () => {
        // 在庫サービスの初期化
        inventoryService = new InventoryService(database);

        // テスト商品の作成
        testProducts = await Promise.all([
          productService.create({
            name: '商品A',
            price: 1000,
            stock: 10,
          }),
          productService.create({
            name: '商品B',
            price: 2000,
            stock: 5,
          }),
        ]);
      });

      test('在庫数を正確に取得できる', async () => {
        const stock = await inventoryService.getStock(
          testProducts[0].id
        );
        expect(stock).toBe(10);
      });

      test('在庫を更新できる', async () => {
        await inventoryService.updateStock(
          testProducts[0].id,
          15
        );
        const newStock = await inventoryService.getStock(
          testProducts[0].id
        );
        expect(newStock).toBe(15);
      });
    });
  });
});

パフォーマンスを考慮した効率的な使い方

重い処理の最適化

重い初期化処理は、beforeAll で実行してパフォーマンスを向上させることができます。

javascriptdescribe('画像処理サービス', () => {
  let imageProcessor;
  let mockImages;

  // 重い初期化は一度だけ実行
  beforeAll(async () => {
    // 画像処理エンジンの初期化(時間がかかる)
    imageProcessor = await ImageProcessor.initialize({
      gpuAcceleration: true,
      memoryLimit: '2GB',
    });

    // 大容量テスト画像の準備
    mockImages = await loadTestImages([
      'test-image-1.jpg',
      'test-image-2.png',
      'test-image-3.gif',
    ]);
  });

  afterAll(async () => {
    // リソースのクリーンアップ
    if (imageProcessor) {
      await imageProcessor.cleanup();
    }
  });

  // 軽い初期化は各テストで実行
  beforeEach(() => {
    imageProcessor.resetSettings();
  });

  test('画像をリサイズできる', async () => {
    const originalImage = mockImages[0];
    const resizedImage = await imageProcessor.resize(
      originalImage,
      { width: 300, height: 200 }
    );

    expect(resizedImage.width).toBe(300);
    expect(resizedImage.height).toBe(200);
  });

  test('画像形式を変換できる', async () => {
    const jpegImage = mockImages[0];
    const pngImage = await imageProcessor.convert(
      jpegImage,
      'png'
    );

    expect(pngImage.format).toBe('png');
  });
});

メモリ使用量の最適化

メモリリークを防ぐため、適切なクリーンアップを実装します。

javascriptdescribe('大容量データ処理', () => {
  let dataStore;
  let processedData;

  beforeEach(() => {
    // 軽量なデータストアの初期化
    dataStore = new Map();
  });

  afterEach(() => {
    // メモリ使用量を削減
    if (dataStore) {
      dataStore.clear();
      dataStore = null;
    }

    if (processedData) {
      processedData = null;
    }

    // ガベージコレクションを促進
    if (global.gc) {
      global.gc();
    }
  });

  test('大量データを効率的に処理する', () => {
    // 大量データの処理
    const inputData = generateLargeDataSet(100000);

    processedData = processLargeData(inputData);

    expect(processedData.length).toBe(100000);

    // テスト終了時に processedData は afterEach でクリアされる
  });
});

並列実行での注意点

Jest の並列実行環境では、リソースの競合を避ける設計が重要です。

javascriptdescribe('並列実行対応ファイル操作', () => {
  let testDirectory;
  let uniquePrefix;

  beforeEach(async () => {
    // 一意なディレクトリ名を生成
    uniquePrefix = `test-${Date.now()}-${Math.random()
      .toString(36)
      .substr(2, 9)}`;
    testDirectory = path.join(
      __dirname,
      'temp',
      uniquePrefix
    );

    await fs.mkdir(testDirectory, { recursive: true });
  });

  afterEach(async () => {
    // 各テスト固有のディレクトリを削除
    try {
      await fs.rmdir(testDirectory, { recursive: true });
    } catch (error) {
      console.warn(
        `テストディレクトリの削除に失敗: ${testDirectory}`,
        error
      );
    }
  });

  test('ファイル作成が競合しない', async () => {
    const fileName = `${uniquePrefix}-test.txt`;
    const filePath = path.join(testDirectory, fileName);

    await fs.writeFile(filePath, 'テストデータ');

    const content = await fs.readFile(filePath, 'utf-8');
    expect(content).toBe('テストデータ');
  });
});

よくある間違いとトラブルシューティング

非同期処理の忘れ

最も一般的な間違いは、非同期処理で await を忘れることです。

javascript// ❌ 悪い例: await が不足
describe('非同期処理の間違い例', () => {
  let database;

  beforeEach(() => {
    // await を忘れている
    database = connectToDatabase(); // Promise が返される
  });

  test('データを取得する', async () => {
    // database はまだ Promise のまま
    const data = await database.query(
      'SELECT * FROM users'
    );
    expect(data).toBeDefined(); // エラーが発生
  });
});

// ✅ 正しい例: async/await を使用
describe('非同期処理の正しい例', () => {
  let database;

  beforeEach(async () => {
    // 正しく await している
    database = await connectToDatabase();
  });

  test('データを取得する', async () => {
    const data = await database.query(
      'SELECT * FROM users'
    );
    expect(data).toBeDefined();
  });
});

モックリセットの忘れ

モック関数のリセットを忘れると、テスト間で状態が漏れる可能性があります。

javascript// ❌ 問題のあるコード
describe('モック管理の問題例', () => {
  const mockFunction = jest.fn();

  // モックリセットがない
  test('最初のテスト', () => {
    mockFunction('引数1');
    expect(mockFunction).toHaveBeenCalledTimes(1);
  });

  test('二番目のテスト', () => {
    mockFunction('引数2');
    // 前のテストの呼び出しが残っているため失敗
    expect(mockFunction).toHaveBeenCalledTimes(1); // 実際は2回
  });
});

// ✅ 修正版
describe('モック管理の正しい例', () => {
  const mockFunction = jest.fn();

  beforeEach(() => {
    // モック状態をリセット
    mockFunction.mockClear();
  });

  test('最初のテスト', () => {
    mockFunction('引数1');
    expect(mockFunction).toHaveBeenCalledTimes(1);
  });

  test('二番目のテスト', () => {
    mockFunction('引数2');
    expect(mockFunction).toHaveBeenCalledTimes(1); // 正常に通る
  });
});

エラー処理の不備

afterEach でのエラーが他のテストに影響することがあります。

javascript// ❌ エラー処理が不十分
describe('エラー処理の問題例', () => {
  let resource;

  beforeEach(async () => {
    resource = await createResource();
  });

  afterEach(async () => {
    // エラーが発生すると後続のテストに影響
    await resource.cleanup(); // この処理が失敗する可能性
  });

  test('テスト1', () => {
    expect(resource).toBeDefined();
  });

  test('テスト2', () => {
    expect(resource).toBeDefined();
  });
});

// ✅ 適切なエラー処理
describe('エラー処理の改善例', () => {
  let resource;

  beforeEach(async () => {
    resource = await createResource();
  });

  afterEach(async () => {
    if (resource) {
      try {
        await resource.cleanup();
      } catch (error) {
        console.warn(
          'リソースクリーンアップに失敗:',
          error
        );
        // エラーを再投げしない(テスト継続のため)
      }
      resource = null;
    }
  });

  test('テスト1', () => {
    expect(resource).toBeDefined();
  });

  test('テスト2', () => {
    expect(resource).toBeDefined();
  });
});

メモリリークの回避

適切でないクリーンアップは、メモリリークの原因となります。

javascriptdescribe('メモリリーク対策', () => {
  let eventEmitter;
  let listeners;

  beforeEach(() => {
    eventEmitter = new EventEmitter();
    listeners = [];
  });

  afterEach(() => {
    // イベントリスナーを適切に削除
    listeners.forEach(({ event, listener }) => {
      eventEmitter.removeListener(event, listener);
    });
    listeners = [];

    // EventEmitterの参照をクリア
    eventEmitter.removeAllListeners();
    eventEmitter = null;
  });

  test('イベントを処理できる', () => {
    const testListener = jest.fn();

    eventEmitter.on('test-event', testListener);
    listeners.push({
      event: 'test-event',
      listener: testListener,
    });

    eventEmitter.emit('test-event', 'データ');
    expect(testListener).toHaveBeenCalledWith('データ');
  });
});

まとめ

Jest の beforeEach・afterEach は、効果的なテスト設計において欠かせない機能です。この記事で解説した内容を活用することで、保守性が高く、安定したテストスイートを構築できるでしょう。

重要なポイントの再確認

テストライフサイクルフックの適切な使用により、テスト間の独立性を保ち、予期しない副作用を防げます。特に、非同期処理や外部リソースを扱うテストでは、正しいセットアップとクリーンアップが不可欠です。

パフォーマンスと保守性の両立

重い処理は beforeAll で一度だけ実行し、軽い処理は beforeEach で毎回実行することで、テスト実行時間を最適化できます。また、適切なエラーハンドリングとメモリ管理により、安定したテスト環境を維持できます。

継続的な改善

テストスイートが成長するにつれて、beforeEach・afterEach の使い方も進化させる必要があります。この記事で学んだベストプラクティスを基に、プロジェクトの特性に応じて最適化を続けていってください。

適切なテストライフサイクル管理により、開発チーム全体の生産性向上と、アプリケーションの品質向上を実現していきましょう。

関連リンク