インテグレーションテスト
ストーリー
「ユニットテストだけでは防げないバグがある」
松本先輩が言った。
「コンポーネント単体は正しくても、繋げた時に 問題が出ることがある。データベースとの連携、 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分