Back

JavaScript におけるユニットテストと統合テスト:いつ何を使うべきか

JavaScript におけるユニットテストと統合テスト:いつ何を使うべきか

信頼性の高いアプリケーションを構築する際、すべての JavaScript 開発者が同じ疑問に直面します。ユニットテストと統合テスト、どちらを多く書くべきか?間違った選択をすると、脆弱なテストのデバッグに何時間も費やすか、本番環境にバグを送り込むことになります。答えはどちらか一方を選ぶことではなく、それぞれがどのような場合に最も価値を発揮するかを理解することです。

この記事では、JavaScript におけるユニットテストと統合テストの違いを明確にし、それぞれの実践的な例を示し、テスト戦略において両者のバランスを取るための判断フレームワークを提供します。

重要なポイント

  • ユニットテストは、速度と精度のために分離されたコードの断片を検証する
  • 統合テストは、実際の環境における信頼性のためにコンポーネント間の相互作用を検証する
  • ほとんどの JavaScript プロジェクトでは、70-20-10 の配分(ユニット-統合-E2E)が機能する
  • テスト対象に基づいて選択する:アルゴリズムにはユニットテスト、ワークフローには統合テストが必要

ユニットテストとは?

ユニットテストは、個々のコードの断片が分離された状態で正しく動作することを検証します。構造物に追加する前に、単一の LEGO ブロックをテストすることを想像してください。

JavaScript では、ユニットテストは通常、以下に焦点を当てます:

  • 単一の関数またはメソッド
  • 子コンポーネントを持たない単一のコンポーネント
  • 特定のクラスまたはモジュール

ユニットテストの例

// calculator.js
export function calculateDiscount(price, percentage) {
  if (percentage < 0 || percentage > 100) {
    throw new Error('Invalid percentage');
  }
  return price * (1 - percentage / 100);
}

// calculator.test.js
import { calculateDiscount } from './calculator';

test('applies 20% discount correctly', () => {
  expect(calculateDiscount(100, 20)).toBe(80);
});

test('throws error for invalid percentage', () => {
  expect(() => calculateDiscount(100, 150)).toThrow('Invalid percentage');
});

ユニットテストが優れている点:

  • 速度: I/O や外部依存関係がないため、テストあたりミリ秒単位
  • 精度: 正確な障害箇所を特定
  • 安定性: 無関係な変更によって壊れることがほとんどない

統合テストとは?

統合テストは、アプリケーションの複数の部分が正しく連携して動作することを検証します。LEGO ブロック単体をテストするのではなく、複数のブロックがどのように接続されるかをテストします。

JavaScript における統合テストは、通常以下をカバーします:

  • API とのコンポーネントの相互作用
  • 複数のモジュールが連携して動作すること
  • ビジネスロジックを伴うデータベース操作
  • 状態管理を伴う UI コンポーネント

統合テストの例

// userProfile.test.js
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';

const server = setupServer(
  rest.get('/api/user/:id', (req, res, ctx) => {
    return res(ctx.json({ 
      id: req.params.id, 
      name: 'Jane Doe',
      role: 'Developer' 
    }));
  })
);

beforeAll(() => server.listen());
afterAll(() => server.close());

test('displays user data after loading', async () => {
  render(<UserProfile userId="123" />);
  
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('Jane Doe')).toBeInTheDocument();
    expect(screen.getByText('Developer')).toBeInTheDocument();
  });
});

統合テストが提供するもの:

  • 信頼性: 実際のユーザーワークフローを検証
  • カバレッジ: コンポーネント間の相互作用をテスト
  • 現実性: ユニットテストでは見逃される問題を捕捉

重要な違い

スコープと分離

ユニットテストは、モックとスタブを使用してコードを分離します。すべての変数を制御できます。 統合テストは、可能な限り実際の実装を使用し、API やデータベースなどの外部境界のみをモック化します。

実行速度

ユニットテストは、それぞれ 1〜50ms で実行されます。数秒で数千のテストを実行できます。 統合テストは、100〜500ms 以上かかります。セットアップ、ティアダウン、場合によっては実際の I/O が含まれます。

メンテナンスコスト

ユニットテストは、特定のユニットが変更された場合にのみ壊れます。 統合テストは、テストされたフロー内のどこかの変更によって壊れる可能性があり、より多くの調査時間が必要です。

バグ検出

ユニットテストは、分離されたコード内のロジックエラーとエッジケースを捕捉します。 統合テストは、配線の問題、誤った仮定、コンポーネント間の契約違反を捕捉します。

それぞれのタイプをいつ使用するか

ユニットテストを書くべき対象:

  • 純粋関数: ビジネスロジック、計算、データ変換
  • 複雑なアルゴリズム: ソート、検索、検証ルール
  • エッジケース: エラー処理、境界条件
  • ユーティリティ関数: フォーマッター、パーサー、ヘルパー

統合テストを書くべき対象:

  • API の相互作用: HTTP リクエスト、レスポンス処理
  • ユーザーワークフロー: 複数ステップのプロセス、フォーム送信
  • コンポーネント統合: 親子コンポーネントの通信
  • 状態管理: Redux アクション、Context API フロー
  • データベース操作: ビジネスロジックを伴う CRUD 操作

実践的なテスト戦略

最も成功している JavaScript プロジェクトのほとんどは、70-20-10 の配分に従っています:

  • 70% ユニットテスト: 高速なフィードバック、簡単なデバッグ
  • 20% 統合テスト: コンポーネント間の相互作用に対する信頼性
  • 10% エンドツーエンドテスト: 重要なパスの最終検証

これは厳格なルールではありません。アプリケーションのタイプに基づいて調整してください。API を多用するアプリには、より多くの統合テストが必要です。アルゴリズムを多用するライブラリには、より多くのユニットテストが必要です。

CI/CD パイプラインへの統合

高速なフィードバックのためにパイプラインを構造化します:

  1. プリコミット: ユニットテストを実行(< 5 秒)
  2. プルリクエスト: すべてのユニットテストと統合テストを実行
  3. デプロイ前: E2E テストを含む完全なテストスイートを実行

Jest によるユニットテスト、Testing Library による統合テスト、MSW による API モックなどのツールにより、このパイプラインを効率的かつメンテナンス可能にします。

避けるべき一般的な落とし穴

  1. 統合テストでの過度なモック化: 相互作用をテストする目的を損なう
  2. 実装の詳細をテストする: 内部構造ではなく、動作に焦点を当てる
  3. テスト速度を無視する: 遅いテストは頻繁な実行を妨げる
  4. バグの後にテストを書く: プロアクティブなテストが問題を防ぐ

結論

JavaScript におけるユニットテストと統合テストは、競合する戦略ではなく、補完的なツールです。ユニットテストは、分離されたロジックに対する速度と精度を提供します。統合テストは、コンポーネントが正しく連携して動作することに対する信頼性を提供します。

ビジネスロジックと純粋関数にはユニットテストから始めましょう。重要なユーザーパスとコンポーネントの相互作用には統合テストを追加しましょう。テスト哲学に関する宗教的な議論はスキップして、コードを出荷する自信を与えるものに焦点を当てましょう。

最良の JavaScript テスト戦略は、開発速度を高く保ちながら、ユーザーよりも先にバグを捕捉するものです。アプリケーションのニーズに基づいて両方のタイプのバランスを取り、最も頻繁に壊れるものを学びながら調整してください。

よくある質問

ユニットテストでは常に外部依存関係をモック化してください。これには、データベース、API、ファイルシステム、その他のサービスが含まれます。モック化により、テストが高速に実行され、予測可能になり、外部システムの動作ではなく、コード自体を真に分離してテストできます。

まず、何が壊れる可能性があるかを自問してください。ロジック自体が複雑な場合は、まずユニットテストを書きます。機能が複数のコンポーネントの相互通信や外部サービスを含む場合は、統合テストを優先します。ほとんどの機能は両方のタイプの恩恵を受けます。

ユニットテストは、スイート全体で 10 秒未満で完了する必要があります。統合テストは 1〜5 分かかることがあります。テストがそれより長くかかる場合は、並列ジョブに分割するか、最適化が必要な遅いテストを特定してください。高速なテストは、開発者が頻繁に実行することを促進します。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay