API結合テスト — フロントからバックエンドまで通して確認する
前章ではUIコンポーネント同士の結合テストを見ました。この章では、もう一段広い範囲 — フロントエンドからバックエンドAPIまで を通して検証する結合テストを扱います。
実務で「結合テスト」と呼ばれるテストの多くは、このレベルを指しています。フォームに入力してAPIを叩き、データベースに保存され、正しいレスポンスが返る — という一連のフローを確認します。
学習者APIのテストって、本物のサーバーやDBを動かすの?それともモックで済ませるの?
APIテストの2つのアプローチ
API結合テストには大きく2つのアプローチがあります。
1. HTTPリクエストベース
実際のHTTPリクエストをAPIサーバーに送信し、レスポンスを検証します。supertest がNode.jsでは定番です。
import request from 'supertest';
import { app } from '../src/app';
test('GET /api/users はユーザー一覧を返す', async () => {
const response = await request(app)
.get('/api/users')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name');
});2. ルートハンドラーの直接呼び出し
Next.jsのRoute Handlersなど、フレームワークが提供する仕組みを使ってハンドラーを直接テストする方法もあります。
import { GET } from '@/app/api/users/route';
import { NextRequest } from 'next/server';
test('GET /api/users はユーザー一覧を返す', async () => {
const request = new NextRequest('http://localhost/api/users');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(2);
});CRUDのテストパターン
APIの基本的なCRUD操作をテストする典型的なパターンです。
取得(GET)
test('ユーザーをIDで取得できる', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).toMatchObject({
id: 1,
name: 'Alice',
email: 'alice@example.com',
});
});
test('存在しないIDは404を返す', async () => {
await request(app)
.get('/api/users/9999')
.expect(404);
});作成(POST)
test('ユーザーを新規作成できる', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'Charlie',
email: 'charlie@example.com',
})
.expect(201);
expect(response.body).toMatchObject({
name: 'Charlie',
email: 'charlie@example.com',
});
expect(response.body.id).toBeDefined();
});バリデーションエラー
test('メールアドレスが空なら400を返す', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Charlie' }) // emailがない
.expect(400);
expect(response.body.error).toBeDefined();
});テストデータの管理

API結合テストで最も手間がかかるのが テストデータの準備と後片付け です。
アプローチ1: テストごとにリセット
各テストの前にデータベースをリセットし、必要なデータを投入します。
beforeEach(async () => {
// テーブルをクリアして初期データを投入
await db.execute('DELETE FROM users');
await db.execute(
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')"
);
});アプローチ2: トランザクションでロールバック
各テストをトランザクション内で実行し、テスト後にロールバックします。データの後片付けが不要です。
let transaction;
beforeEach(async () => {
transaction = await db.beginTransaction();
});
afterEach(async () => {
await transaction.rollback();
});アプローチ3: テスト用データベース
テスト専用のデータベースを用意し、テストスイートの前後でマイグレーションとシードを実行します。
beforeAll(async () => {
await runMigrations(testDb);
await seedTestData(testDb);
});
afterAll(async () => {
await testDb.close();
});認証が絡むテスト
認証が必要なAPIをテストする場合、テスト用のトークンやセッションを準備します。
ヘルパー関数で認証を共通化
async function authenticatedRequest(app) {
const agent = request.agent(app);
// ログインしてセッションを取得
await agent
.post('/api/auth/login')
.send({ email: 'alice@example.com', password: 'password123' });
return agent; // 以降のリクエストにCookieが含まれる
}
test('認証済みユーザーはプロフィールを取得できる', async () => {
const agent = await authenticatedRequest(app);
const response = await agent
.get('/api/profile')
.expect(200);
expect(response.body.name).toBe('Alice');
});
test('未認証だと401を返す', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});フロント+APIの通しテスト
前章のコンポーネント結合テストでは、APIをMSWでモックしました。ここではさらに踏み込んで、実際のAPIサーバーを起動して フロントからAPIまで通すテストを考えます。
// テスト用のAPIサーバーを起動してテスト
let server;
beforeAll(async () => {
server = await startTestServer({ port: 4000 });
});
afterAll(async () => {
await server.close();
});
test('フォームから送信したデータがAPIに保存される', async () => {
const user = userEvent.setup();
render(<CreateUserPage apiBase="http://localhost:4000" />);
await user.type(screen.getByLabelText('名前'), 'Charlie');
await user.type(screen.getByLabelText('メールアドレス'), 'charlie@example.com');
await user.click(screen.getByRole('button', { name: '作成' }));
// 成功メッセージが表示される
await waitFor(() => {
expect(screen.getByText('作成しました')).toBeInTheDocument();
});
// 実際にDBに保存されたか確認
const response = await fetch('http://localhost:4000/api/users?email=charlie@example.com');
const data = await response.json();
expect(data[0].name).toBe('Charlie');
});このレベルのテストはE2Eに近く、セットアップの手間がかかりますが、MSWでは見つけられない「実際のAPIとの不整合」を検出できます。
まとめ
- API結合テストは HTTPリクエストベース と ルートハンドラー直接呼び出し の2つのアプローチがある
- テストデータは テストごとにリセット するのが基本。テスト間の依存は避ける
- 認証が絡むテストは ヘルパー関数で共通化 する
- フロント+APIの通しテストはE2Eに近いが、MSWでは見つけられない不整合 を検出できる
次の章では、テストの粒度をさらに広げて、ブラウザを使った E2Eテスト の基本を見ていきます。