G2 Best Software 2026、急成長部門一覧を見る
API Testing4 min readUpdated February 2026

API統合テスト:戦略、ツール、ベストプラクティス

S
Technical Writer, Qodex
API統合テスト:戦略、ツール、ベストプラクティス

はじめに

ユニットテストは個々の関数を検証します。APIテストは個々のエンドポイントを検証します。しかし、どちらもサービスが実際に連携して動作するかどうかを教えてはくれません。それが統合テストの役割です。

API統合テストは、複数のサービス、データベース、外部システムがAPIインターフェースを通じて正しく通信することを検証します。数十のサービスが互いに依存するマイクロサービスアーキテクチャでは、統合テストは契約の不一致、ネットワーク障害、データのシリアライゼーション問題を捕まえるセーフティネットです。

このガイドでは、シンプルな2サービスのシナリオから複雑なマルチサービスワークフローまで、信頼性の高いAPI統合テストを構築するために必要な戦略、ツール、コード例、ベストプラクティスを説明します。

API統合テストとは?

API統合テストは、接続されたシステムがAPIインターフェースを通じて正しく連携することを検証します。ユニットテスト(コードを分離してテスト)やエンドツーエンドテスト(UIを通じてシステム全体をテスト)とは異なり、統合テストはサービス間の境界に焦点を当てます。

統合テストで検証すること

  • データが正しく流れること(サービスAがサービスBで解析できるデータを送信する)
  • コントラクトが守られること(APIがコンシューマーが期待するフィールドと型を返す)
  • エラー処理がサービス境界をまたいで機能すること(サービスAがサービスBのエラーを適切に処理する)
  • 認証がサービスチェーンを通じて正しく伝播すること
  • データベース操作が正しく機能すること(クエリ、トランザクション、マイグレーション)
  • 外部APIが期待通りに動作すること(決済ゲートウェイ、メールサービス、サードパーティデータ)

統合テストと他のテストタイプの比較

テストタイプスコープ速度依存関係検出内容
ユニットテスト単一関数/クラス速い(ms)モックロジックのバグ
APIテスト単一エンドポイント速い(ms-s)多くの場合モックAPIコントラクト違反
統合テスト複数サービス中程度(s)実物またはコンテナインターフェースの不一致
E2Eテストシステム全体+UI遅い(min)すべて実物ユーザーワークフローのバグ

API統合テストの戦略

戦略1:ビッグバン統合テスト

すべてのサービスを一度に接続してシステム全体をテストします。理解しやすいですが、テストが失敗したときのデバッグが難しく、どのサービスが失敗を引き起こしたかがわかりません。

最適な場面: サービスが少ない小規模システム。

戦略2:インクリメンタル統合テスト

サービスを一つずつ追加してテストします。コアサービスから始め、接続されたサービスを段階的に追加します。

トップダウン: APIゲートウェイから始めてダウンストリームサービスをモックし、モックを一つずつ実際のサービスに置き換えます。

ボトムアップ: 最下層のサービス(データベース、キャッシュ)から始めて上に向かって構築します。

サンドイッチ: トップダウンとボトムアップを組み合わせ、中間で合流します。

最適な場面: 障害を分離する必要がある中大規模システム。

戦略3:コントラクトテスト

コンシューマーとプロバイダー間のコントラクトを定義し、両側を独立して検証します。これはマイクロサービスに最もスケーラブルなアプローチです。

最適な場面: 多くのサービス間依存関係を持つマイクロサービスアーキテクチャ。

コードを使った実践的なAPI統合テスト

JavaScript:Supertestを使ったサービス統合テスト

// tests/integration/orders.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../../src/db');

describe('Orders API Integration', () => {
  let userId;
  let productId;

  beforeAll(async () => {
    // Seed database with test data
    await db.migrate.latest();
    const user = await db('users').insert({
      name: 'Test User',
      email: 'test@example.com'
    }).returning('id');
    userId = user[0].id;

    const product = await db('products').insert({
      name: 'Widget',
      price: 29.99,
      stock: 100
    }).returning('id');
    productId = product[0].id;
  });

  afterAll(async () => {
    await db('orders').del();
    await db('products').del();
    await db('users').del();
    await db.destroy();
  });

  test('Creating an order updates product stock', async () => {
    // Create order via API
    const orderRes = await request(app)
      .post('/api/orders')
      .send({
        userId,
        items: [{ productId, quantity: 3 }]
      })
      .expect(201);

    expect(orderRes.body.total).toBe(89.97); // 29.99 * 3

    // Verify stock was decremented
    const productRes = await request(app)
      .get(`/api/products/${productId}`)
      .expect(200);

    expect(productRes.body.stock).toBe(97); // 100 - 3
  });

  test('Order fails when insufficient stock', async () => {
    const res = await request(app)
      .post('/api/orders')
      .send({
        userId,
        items: [{ productId, quantity: 9999 }]
      })
      .expect(400);

    expect(res.body.error).toContain('Insufficient stock');
  });

  test('Order creation sends notification to user service', async () => {
    const orderRes = await request(app)
      .post('/api/orders')
      .send({
        userId,
        items: [{ productId, quantity: 1 }]
      })
      .expect(201);

    // Verify notification was created
    const notifRes = await request(app)
      .get(`/api/users/${userId}/notifications`)
      .expect(200);

    const orderNotif = notifRes.body.find(
      n => n.type === 'order_confirmation'
    );
    expect(orderNotif).toBeDefined();
    expect(orderNotif.orderId).toBe(orderRes.body.id);
  });
});

Python:pytestとDockerを使ったテスト

# tests/integration/test_order_flow.py
import pytest
import requests
import time

API_URL = "http://localhost:3000/api"

@pytest.fixture(scope="module")
def test_user():
    """Create a test user and return their data."""
    response = requests.post(f"{API_URL}/users", json={
        "name": "Integration Test User",
        "email": "integration@test.com"
    })
    assert response.status_code == 201
    yield response.json()
    # Cleanup
    requests.delete(f"{API_URL}/users/{response.json()['id']}")

@pytest.fixture(scope="module")
def test_product():
    """Create a test product."""
    response = requests.post(f"{API_URL}/products", json={
        "name": "Test Widget",
        "price": 19.99,
        "stock": 50
    })
    assert response.status_code == 201
    yield response.json()
    requests.delete(f"{API_URL}/products/{response.json()['id']}")

class TestOrderIntegration:
    def test_complete_order_flow(self, test_user, test_product):
        """Test the full order lifecycle across services."""
        # Step 1: Create order
        order_response = requests.post(f"{API_URL}/orders", json={
            "userId": test_user["id"],
            "items": [{"productId": test_product["id"], "quantity": 2}]
        })
        assert order_response.status_code == 201
        order = order_response.json()
        assert order["total"] == 39.98  # 19.99 * 2

        # Step 2: Verify payment was processed
        payment_response = requests.get(
            f"{API_URL}/orders/{order['id']}/payment"
        )
        assert payment_response.status_code == 200
        assert payment_response.json()["status"] == "completed"

        # Step 3: Verify inventory updated
        product_response = requests.get(
            f"{API_URL}/products/{test_product['id']}"
        )
        assert product_response.status_code == 200
        assert product_response.json()["stock"] == 48  # 50 - 2

    def test_order_rollback_on_payment_failure(self, test_user, test_product):
        """Verify stock is restored when payment fails."""
        initial_stock = requests.get(
            f"{API_URL}/products/{test_product['id']}"
        ).json()["stock"]

        # Create order with invalid payment method to trigger failure
        order_response = requests.post(f"{API_URL}/orders", json={
            "userId": test_user["id"],
            "items": [{"productId": test_product["id"], "quantity": 1}],
            "paymentMethod": "invalid_card"
        })
        assert order_response.status_code == 400

        # Verify stock was not decremented
        current_stock = requests.get(
            f"{API_URL}/products/{test_product['id']}"
        ).json()["stock"]
        assert current_stock == initial_stock

Pactを使ったコントラクトテスト

コントラクトテストはマイクロサービスでのAPI統合テストに最も効果的なアプローチです。コンシューマーがプロバイダーから期待するものを定義し、両側が独立して検証します。

コンシューマー側テスト(JavaScript)

// consumer/tests/userServiceClient.pact.test.js
const { PactV3 } = require('@pact-foundation/pact');
const { UserServiceClient } = require('../src/userServiceClient');

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'UserService',
});

describe('UserService Client', () => {
  test('fetches user by ID', async () => {
    provider
      .given('a user with ID 1 exists')
      .uponReceiving('a request for user 1')
      .withRequest({
        method: 'GET',
        path: '/api/users/1',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com',
        },
      });

    await provider.executeTest(async (mockServer) => {
      const client = new UserServiceClient(mockServer.url);
      const user = await client.getUser(1);

      expect(user.id).toBe(1);
      expect(user.name).toBe('John Doe');
    });
  });
});

プロバイダー側検証

// provider/tests/pactVerification.test.js
const { Verifier } = require('@pact-foundation/pact');

describe('UserService Provider Verification', () => {
  test('validates contract with OrderService', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://localhost:3001',
      pactUrls: ['./pacts/OrderService-UserService.json'],
      stateHandlers: {
        'a user with ID 1 exists': async () => {
          // Set up the required state in the provider
          await db('users').insert({
            id: 1,
            name: 'John Doe',
            email: 'john@example.com',
          });
        },
      },
    });

    await verifier.verifyProvider();
  });
});

統合テストへのDockerの活用

統合テストには実際の依存関係(データベース、キャッシュ、メッセージキュー)が必要です。Docker Composeがこれを管理しやすくします:

# docker-compose.test.yml
version: '3.8'
services:
  api:
    build: .
    environment:
      DATABASE_URL: postgres://test:test@db:5432/testdb
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    healthcheck:
      test: pg_isready -U test
      interval: 5s
      retries: 5

  cache:
    image: redis:7-alpine

  test-runner:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      API_URL: http://api:3000
    depends_on:
      - api
    command: npm run test:integration
# Run integration tests
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit

CI/CDにおける統合テスト

# GitHub Actions integration tests
name: Integration Tests
on:
  push:
    branches: [main, develop]

jobs:
  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run migrate
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/testdb
      - run: npm run test:integration
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379

API統合テストのベストプラクティス

1. 可能な限り実際の依存関係を使用する

モックされた依存関係を持つ統合テストは統合テストではありません。それは偽装したユニットテストです。Dockerコンテナを介して実際のデータベース、実際のキャッシュ、実際のメッセージキューを使用してください。

2. テストデータを分離する

各テストは独自のデータを作成し、後でクリーンアップする必要があります。各テスト後にロールバックするデータベーストランザクションを使用するか、テスト実行間でテーブルをトランケートします。

3. エラーシナリオをテストする

ハッピーパスだけをテストしないでください。依存関係が利用不能な場合、エラーを返す場合、または予期しないデータを返す場合に何が起こるかをテストしてください。

4. テストを速く保つ

統合テストはユニットテストより遅いですが、遅くなりすぎてはいけません。統合スイート全体で30秒未満を目標にしてください。安全な場合は並列実行と共有セットアップを使用してください。

5. クロスチームAPIにはコントラクトテストを使用する

異なるチームが異なるサービスを所有する場合、コントラクトテスト(Pactや類似のもの)はすべてのサービスを一緒に実行するよりも実用的です。

6. 他のテストタイプと組み合わせる

統合テストはREST APIテスト負荷テストセキュリティテストを補完します。Qodex.aiを使用して機能テストとセキュリティテストを自動生成し、クロスサービスワークフローの統合テストを追加してください。

ツールの完全な概要については、APIテストツール比較をご覧ください。


よくある質問

APIテストとAPI統合テストの違いは何ですか?

APIテストは単一のAPIエンドポイントを分離して検証します。正しいステータスコード、レスポンスボディ、エラー処理です。API統合テストは複数のサービスがAPIを通じて連携することを検証します。データが正しく流れること、コントラクトが守られること、エラーがサービス境界を適切に伝播することです。

APIテストは統合テストと同じですか?

正確には異なります。APIテストは分離して(依存関係をモックして)行えるため、ユニットテストに近いです。統合テストは特に実際のサービス、データベース、外部システム間のインタラクションをテストします。ただし、実際の依存関係に対してテストする場合、多くのチームがAPIテストを統合テストとして使用するため、大きな重複があります。

実際のサービスへのアクセスなしにAPI統合をテストするにはどうすればよいですか?

Pactなどのツールを使ったコントラクトテストを使用してください。コンシューマーが期待するインタラクションを定義し、両側が独立して検証します。モックサーバー、WireMock、またはサービス仮想化ツールを使用して外部サービスをシミュレートすることもできます。

API統合テストに最適なツールは何ですか?

JavaScript向け:DockerでSupertest + Jest。Python向け:DockerでpytestとRequests。コントラクトテスト向け:Pact。自動テスト生成向け:Qodex.ai。クロスサービステスト向け:すべてのサービスを調整するDocker Compose。

統合テストでテストデータをどのように扱いますか?

各テスト後にロールバックするデータベーストランザクションを使用するか、テスト実行間でテーブルをトランケートします。setUp/beforeEachフックでテストデータを作成し、tearDown/afterEachでクリーンアップします。テスト間でテストデータを共有しないでください。各テストは独立している必要があります。

統合テストはCI/CDで実行すべきですか?

はい。統合テストはすべてのプッシュとプルリクエストで実行すべきです。Docker ComposeまたはCI/CDサービスコンテナ(GitHub Actionsサービスなど)を使用して実際の依存関係を起動します。重要な統合パスに集中してテストを並列実行することでスイートを速く保ってください。