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サービスなど)を使用して実際の依存関係を起動します。重要な統合パスに集中してテストを並列実行することでスイートを速く保ってください。





