LESSON 30分

インテグレーションテスト

ストーリー

「ユニットテストだけでは防げないバグがある」

松本先輩が言った。

「コンポーネント単体は正しくても、繋げた時に 問題が出ることがある。データベースとの連携、 APIのリクエスト/レスポンス...... インテグレーションテストはその隙間を埋めるんだ」


インテグレーションテストとは

複数のコンポーネント(モジュール、サービス、データベースなど)が正しく連携して動作することを検証するテストです。

ユニットテスト:        インテグレーションテスト:
┌────┐                ┌────┐  ←→  ┌────┐
│ A  │ ← テスト       │ A  │      │ DB │ ← テスト
└────┘                └────┘  ←→  └────┘
  ↑                     ↑
  モック使用             実際の依存を使用

APIインテグレーションテスト

Express + supertest

typescript
// src/app.ts
import express from 'express';
import { userRouter } from './routes/userRouter';

export const app = express();
app.use(express.json());
app.use('/api/users', userRouter);
typescript
// tests/integration/users.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import { setupTestDatabase, cleanupTestDatabase } from '../helpers/database';

describe('GET /api/users', () => {
  beforeAll(async () => {
    await setupTestDatabase();
  });

  afterAll(async () => {
    await cleanupTestDatabase();
  });

  beforeEach(async () => {
    // テストデータを投入
    await seedUsers([
      { name: '田中太郎', email: 'tanaka@example.com' },
      { name: '鈴木花子', email: 'suzuki@example.com' },
    ]);
  });

  afterEach(async () => {
    await clearUsers();
  });

  it('ユーザー一覧を取得できる', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect(200);

    expect(response.body).toHaveLength(2);
    expect(response.body[0]).toHaveProperty('name');
    expect(response.body[0]).toHaveProperty('email');
    expect(response.body[0]).not.toHaveProperty('passwordHash');
  });

  it('名前でユーザーを検索できる', async () => {
    const response = await request(app)
      .get('/api/users?name=田中')
      .expect(200);

    expect(response.body).toHaveLength(1);
    expect(response.body[0].name).toBe('田中太郎');
  });

  it('存在しないユーザーIDで404が返る', async () => {
    const response = await request(app)
      .get('/api/users/nonexistent-id')
      .expect(404);

    expect(response.body.error).toBe('User not found');
  });
});

describe('POST /api/users', () => {
  beforeAll(async () => {
    await setupTestDatabase();
  });

  afterAll(async () => {
    await cleanupTestDatabase();
  });

  afterEach(async () => {
    await clearUsers();
  });

  it('新規ユーザーを作成できる', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: '佐藤一郎',
        email: 'sato@example.com',
        password: 'SecurePass123!',
      })
      .expect(201);

    expect(response.body.id).toBeDefined();
    expect(response.body.name).toBe('佐藤一郎');
    expect(response.body.email).toBe('sato@example.com');
    expect(response.body).not.toHaveProperty('password');
  });

  it('不正なメールアドレスで400が返る', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: '佐藤一郎',
        email: 'invalid-email',
        password: 'SecurePass123!',
      })
      .expect(400);

    expect(response.body.error).toContain('email');
  });

  it('重複するメールアドレスで409が返る', async () => {
    // 1人目を作成
    await request(app)
      .post('/api/users')
      .send({
        name: '佐藤一郎',
        email: 'sato@example.com',
        password: 'SecurePass123!',
      })
      .expect(201);

    // 同じメールで2人目を作成
    const response = await request(app)
      .post('/api/users')
      .send({
        name: '佐藤二郎',
        email: 'sato@example.com',
        password: 'AnotherPass123!',
      })
      .expect(409);

    expect(response.body.error).toContain('already exists');
  });
});

データベーステスト

テスト用データベースの管理

typescript
// tests/helpers/database.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({
  datasources: {
    db: { url: process.env.TEST_DATABASE_URL },
  },
});

export async function setupTestDatabase() {
  // マイグレーションの実行(テスト用DB)
  await prisma.$executeRaw`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
}

export async function cleanupTestDatabase() {
  await prisma.$disconnect();
}

export async function clearAllTables() {
  // テーブルの全データを削除(テスト間の独立性を保つ)
  await prisma.$transaction([
    prisma.orderItem.deleteMany(),
    prisma.order.deleteMany(),
    prisma.user.deleteMany(),
  ]);
}

トランザクションを使った高速化

typescript
// 各テストをトランザクションで包んでロールバック
describe('UserRepository', () => {
  let prisma: PrismaClient;

  beforeEach(async () => {
    // トランザクション開始
    await prisma.$executeRaw`BEGIN`;
  });

  afterEach(async () => {
    // テストデータをロールバック(高速)
    await prisma.$executeRaw`ROLLBACK`;
  });

  it('ユーザーを保存して取得できる', async () => {
    const repo = new UserRepository(prisma);

    await repo.save({
      name: '田中太郎',
      email: 'tanaka@example.com',
    });

    const found = await repo.findByEmail('tanaka@example.com');
    expect(found).not.toBeNull();
    expect(found!.name).toBe('田中太郎');
  });
});

インテグレーションテストのベストプラクティス

プラクティス説明
テスト用DB本番DBとは別のテスト専用DBを使用
データの独立性各テストの前後でデータをクリーンアップ
現実的なデータテストデータは本番に近い値を使用
CI環境での実行Docker Compose でDB含めた環境を構築
実行順序に依存しないテストはどの順序でも動くように設計

CI での実行例

yaml
# .github/workflows/test.yml
name: Integration Tests
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_pass
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
      - run: npm run test:integration
        env:
          TEST_DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db

まとめ

項目ポイント
目的コンポーネント間の連携を検証する
API テストsupertest でHTTPリクエストをシミュレート
DB テストテスト用DBを使用し、テスト間のデータを独立させる
CI統合Docker Compose や GitHub Actions services で環境構築

チェックリスト

  • supertest を使ったAPIテストを書ける
  • テスト用データベースのセットアップ・クリーンアップができる
  • テスト間のデータ独立性を確保できる
  • CI環境でのインテグレーションテスト実行方法を理解した

次のステップへ

インテグレーションテストを学んだら、次はPlaywrightを使ったE2Eテストを学びます。ブラウザ上のユーザー操作をシミュレートするテストを書いていきましょう。


推定読了時間: 30分