APIテストの作成・メンテナンスに追われていませんか?
- 「エンドポイントが増えるたびにテストの更新が大変」
- 「認証やページネーションのテストが複雑すぎる」
- 「APIの仕様変更でテストが大量に壊れる」
**Gemini CLI(Google Antigravity)を活用すれば、APIテストの設計・生成・実行を劇的に効率化できます。**OpenAPI仕様やGraphQLスキーマからテストを自動生成し、CI/CDパイプラインに組み込むところまで、この記事で完全にカバーします。

APIテスト自動化の全体像
テストピラミッドにおけるAPIテストの位置づけ
テストピラミッドの中層に位置するAPIテスト(統合テスト)は、E2Eテストよりも高速で安定し、ユニットテストよりも広範な動作を検証できる、費用対効果の高いテスト手法です。
┌─────────┐
│ E2E │ 少数・高コスト
├─────────┤
│ API │ ← Gemini CLIが最も力を発揮
├─────────┤
│ Unit │ 多数・低コスト
└─────────┘
Gemini CLIによるAPIテスト自動化のメリット
| 機能 | 手動テスト作成 | Gemini CLI自動化 |
|---|---|---|
| テスト設計 | 仕様書を読んで手動設計 | スキーマから自動設計 |
| テストデータ生成 | 手動でダミーデータ作成 | 境界値含む自動生成 |
| 異常系カバー | 見落としがち | 網羅的に検出 |
| 認証テスト | 毎回手動設定 | 認証フロー自動化 |
| レスポンス検証 | 手動でアサーション記述 | スキーマベースで自動 |
| メンテナンス | API変更ごとに手動修正 | 差分検出で自動提案 |
REST APIテストの自動生成
OpenAPI仕様からのテスト生成
Gemini CLIの最大の強みは、OpenAPI(Swagger)仕様を分析して包括的なテストケースを自動生成できることです。
gemini "openapi.yamlを読み込んで、
全エンドポイントに対するAPIテストを生成して。
テストフレームワークはJest + supertest。
以下のテストパターンを網羅:
- 正常系(200レスポンス)
- バリデーションエラー(400)
- 認証エラー(401/403)
- 存在しないリソース(404)
- サーバーエラー処理(500)"
Gemini CLIは以下のようなテストを自動生成します。
// tests/api/users.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import { setupTestDb, teardownTestDb, createTestUser } from '../helpers/db';
describe('Users API', () => {
beforeAll(async () => {
await setupTestDb();
});
afterAll(async () => {
await teardownTestDb();
});
describe('GET /api/v1/users', () => {
it('認証済みユーザーが一覧を取得できる', async () => {
const token = await getAuthToken('[email protected]');
const response = await request(app)
.get('/api/v1/users')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body).toHaveProperty('data');
expect(response.body).toHaveProperty('pagination');
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.pagination).toMatchObject({
page: expect.any(Number),
limit: expect.any(Number),
total: expect.any(Number),
});
});
it('ページネーションが正しく動作する', async () => {
const token = await getAuthToken('[email protected]');
// 5件ずつ取得
const page1 = await request(app)
.get('/api/v1/users?page=1&limit=5')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(page1.body.data).toHaveLength(5);
expect(page1.body.pagination.page).toBe(1);
const page2 = await request(app)
.get('/api/v1/users?page=2&limit=5')
.set('Authorization', `Bearer ${token}`)
.expect(200);
// ページ間でデータが重複しないことを確認
const page1Ids = page1.body.data.map((u: any) => u.id);
const page2Ids = page2.body.data.map((u: any) => u.id);
expect(page1Ids).not.toEqual(expect.arrayContaining(page2Ids));
});
it('未認証リクエストは401を返す', async () => {
await request(app)
.get('/api/v1/users')
.expect(401);
});
it('一般ユーザーは管理者一覧にアクセスできない', async () => {
const token = await getAuthToken('[email protected]');
await request(app)
.get('/api/v1/users?role=admin')
.set('Authorization', `Bearer ${token}`)
.expect(403);
});
});
describe('POST /api/v1/users', () => {
it('有効なデータでユーザーを作成できる', async () => {
const token = await getAuthToken('[email protected]');
const newUser = {
name: 'テストユーザー',
email: `test-${Date.now()}@example.com`,
role: 'member',
};
const response = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${token}`)
.send(newUser)
.expect(201);
expect(response.body.data).toMatchObject({
name: newUser.name,
email: newUser.email,
role: newUser.role,
});
expect(response.body.data).toHaveProperty('id');
expect(response.body.data).toHaveProperty('createdAt');
});
it('メールアドレスが不正な場合400を返す', async () => {
const token = await getAuthToken('[email protected]');
const response = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${token}`)
.send({ name: 'テスト', email: 'invalid-email', role: 'member' })
.expect(400);
expect(response.body.errors).toContainEqual(
expect.objectContaining({
field: 'email',
message: expect.stringContaining('メールアドレス'),
})
);
});
it('名前が空の場合400を返す', async () => {
const token = await getAuthToken('[email protected]');
const response = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${token}`)
.send({ name: '', email: '[email protected]', role: 'member' })
.expect(400);
expect(response.body.errors).toContainEqual(
expect.objectContaining({ field: 'name' })
);
});
it('重複するメールアドレスは409を返す', async () => {
const token = await getAuthToken('[email protected]');
const existingUser = await createTestUser();
await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${token}`)
.send({
name: '重複テスト',
email: existingUser.email,
role: 'member',
})
.expect(409);
});
});
describe('PUT /api/v1/users/:id', () => {
it('ユーザー情報を更新できる', async () => {
const token = await getAuthToken('[email protected]');
const user = await createTestUser();
const response = await request(app)
.put(`/api/v1/users/${user.id}`)
.set('Authorization', `Bearer ${token}`)
.send({ name: '更新後の名前' })
.expect(200);
expect(response.body.data.name).toBe('更新後の名前');
});
it('存在しないユーザーの更新は404を返す', async () => {
const token = await getAuthToken('[email protected]');
await request(app)
.put('/api/v1/users/non-existent-id')
.set('Authorization', `Bearer ${token}`)
.send({ name: 'テスト' })
.expect(404);
});
});
describe('DELETE /api/v1/users/:id', () => {
it('ユーザーを削除できる', async () => {
const token = await getAuthToken('[email protected]');
const user = await createTestUser();
await request(app)
.delete(`/api/v1/users/${user.id}`)
.set('Authorization', `Bearer ${token}`)
.expect(204);
// 削除後に取得すると404
await request(app)
.get(`/api/v1/users/${user.id}`)
.set('Authorization', `Bearer ${token}`)
.expect(404);
});
it('自分自身は削除できない', async () => {
const admin = await createTestUser({ role: 'admin' });
const token = await getAuthToken(admin.email);
await request(app)
.delete(`/api/v1/users/${admin.id}`)
.set('Authorization', `Bearer ${token}`)
.expect(400);
});
});
});
認証フローのテスト自動化
gemini "OAuth2.0 + JWTの認証フロー全体のテストを生成して。
以下のフローをカバー:
- ログイン → トークン取得
- リフレッシュトークンによるトークン更新
- トークン失効
- 不正トークンでのアクセス
- レート制限のテスト"
// tests/api/auth.test.ts
describe('認証フロー', () => {
describe('POST /api/v1/auth/login', () => {
it('正しい認証情報でJWTトークンを取得', async () => {
const response = await request(app)
.post('/api/v1/auth/login')
.send({
email: '[email protected]',
password: 'correct-password',
})
.expect(200);
expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
expect(response.body).toHaveProperty('expiresIn');
// JWTの構造検証
const decoded = jwt.decode(response.body.accessToken);
expect(decoded).toMatchObject({
sub: expect.any(String),
role: expect.any(String),
iat: expect.any(Number),
exp: expect.any(Number),
});
});
it('間違ったパスワードで401', async () => {
await request(app)
.post('/api/v1/auth/login')
.send({
email: '[email protected]',
password: 'wrong-password',
})
.expect(401);
});
it('5回連続失敗でアカウントロック', async () => {
for (let i = 0; i < 5; i++) {
await request(app)
.post('/api/v1/auth/login')
.send({
email: '[email protected]',
password: 'wrong-password',
});
}
// 正しいパスワードでもロック
const response = await request(app)
.post('/api/v1/auth/login')
.send({
email: '[email protected]',
password: 'correct-password',
})
.expect(423);
expect(response.body.message).toContain('アカウントがロック');
});
});
describe('POST /api/v1/auth/refresh', () => {
it('リフレッシュトークンで新しいアクセストークンを取得', async () => {
const loginRes = await request(app)
.post('/api/v1/auth/login')
.send({ email: '[email protected]', password: 'correct-password' });
const response = await request(app)
.post('/api/v1/auth/refresh')
.send({ refreshToken: loginRes.body.refreshToken })
.expect(200);
expect(response.body).toHaveProperty('accessToken');
expect(response.body.accessToken).not.toBe(loginRes.body.accessToken);
});
it('使用済みリフレッシュトークンは無効化', async () => {
const loginRes = await request(app)
.post('/api/v1/auth/login')
.send({ email: '[email protected]', password: 'correct-password' });
// 1回目のリフレッシュ(成功)
await request(app)
.post('/api/v1/auth/refresh')
.send({ refreshToken: loginRes.body.refreshToken })
.expect(200);
// 2回目は失敗(トークンローテーション)
await request(app)
.post('/api/v1/auth/refresh')
.send({ refreshToken: loginRes.body.refreshToken })
.expect(401);
});
});
});
GraphQLテストの自動生成
スキーマからのテスト生成
GraphQL APIのテストは、スキーマ定義から自動生成できます。
gemini "schema.graphqlを分析して、
全Query・Mutation・Subscriptionの
テストケースを生成して。以下を含めて:
- 各フィールドのresolver検証
- ネストしたクエリの動作確認
- 引数のバリデーション
- N+1問題の検出テスト
- エラーハンドリング"
// tests/graphql/users.test.ts
import { createTestClient } from 'apollo-server-testing';
import { gql } from 'graphql-tag';
import { server } from '../../src/graphql/server';
describe('GraphQL Users', () => {
const { query, mutate } = createTestClient(server);
describe('Query: users', () => {
it('ユーザー一覧を取得できる', async () => {
const GET_USERS = gql`
query GetUsers($first: Int, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
role
createdAt
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
`;
const result = await query({
query: GET_USERS,
variables: { first: 10 },
});
expect(result.errors).toBeUndefined();
expect(result.data.users.edges).toBeDefined();
expect(result.data.users.totalCount).toBeGreaterThan(0);
expect(result.data.users.pageInfo).toHaveProperty('hasNextPage');
});
it('カーソルベースのページネーションが正しく動作', async () => {
const GET_USERS = gql`
query GetUsers($first: Int, $after: String) {
users(first: $first, after: $after) {
edges {
node { id name }
cursor
}
pageInfo { hasNextPage endCursor }
}
}
`;
const page1 = await query({
query: GET_USERS,
variables: { first: 2 },
});
const page2 = await query({
query: GET_USERS,
variables: {
first: 2,
after: page1.data.users.pageInfo.endCursor,
},
});
const page1Ids = page1.data.users.edges.map((e: any) => e.node.id);
const page2Ids = page2.data.users.edges.map((e: any) => e.node.id);
expect(page1Ids).not.toEqual(expect.arrayContaining(page2Ids));
});
});
describe('Mutation: createUser', () => {
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
role
}
}
`;
it('有効な入力でユーザーを作成', async () => {
const result = await mutate({
mutation: CREATE_USER,
variables: {
input: {
name: 'GraphQLテスト太郎',
email: `gql-test-${Date.now()}@example.com`,
role: 'MEMBER',
},
},
});
expect(result.errors).toBeUndefined();
expect(result.data.createUser).toMatchObject({
name: 'GraphQLテスト太郎',
role: 'MEMBER',
});
});
it('不正な入力でバリデーションエラー', async () => {
const result = await mutate({
mutation: CREATE_USER,
variables: {
input: {
name: '',
email: 'not-an-email',
role: 'INVALID_ROLE',
},
},
});
expect(result.errors).toBeDefined();
expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT');
});
});
describe('N+1問題の検出', () => {
it('ユーザーの所属プロジェクト取得でN+1が発生しない', async () => {
const queryLogger = jest.fn();
// SQLクエリをモニタリング
prisma.$on('query', queryLogger);
const USERS_WITH_PROJECTS = gql`
query {
users(first: 10) {
edges {
node {
id
name
projects {
id
name
}
}
}
}
}
`;
await query({ query: USERS_WITH_PROJECTS });
// DataLoaderが機能していれば、クエリ数は3以下
// (users + projects のバッチ + count)
expect(queryLogger).toHaveBeenCalledTimes(
expect.lessThanOrEqual(3)
);
});
});
});
負荷テストとパフォーマンス検証
k6によるAPIロードテスト
Gemini CLIでk6の負荷テストスクリプトを自動生成できます。
gemini "openapi.yamlの主要エンドポイントに対する
k6負荷テストスクリプトを生成して。
以下のシナリオ:
- スモークテスト: 1VU、10秒
- ロードテスト: 50VU、5分
- ストレステスト: 100VU→200VU、10分(段階的増加)
各シナリオのしきい値も設定して"
// tests/load/api-load-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const loginDuration = new Trend('login_duration');
export const options = {
scenarios: {
smoke: {
executor: 'constant-vus',
vus: 1,
duration: '10s',
tags: { test_type: 'smoke' },
},
load: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
startTime: '15s',
tags: { test_type: 'load' },
},
stress: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
startTime: '5m30s',
tags: { test_type: 'stress' },
},
},
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
errors: ['rate<0.05'],
},
};
const BASE_URL = __ENV.API_URL || 'http://localhost:3000';
export default function () {
let token;
group('認証', () => {
const loginStart = Date.now();
const loginRes = http.post(`${BASE_URL}/api/v1/auth/login`, JSON.stringify({
email: '[email protected]',
password: 'loadtest-password',
}), { headers: { 'Content-Type': 'application/json' } });
loginDuration.add(Date.now() - loginStart);
check(loginRes, { 'ログイン成功': (r) => r.status === 200 });
token = loginRes.json('accessToken');
});
const headers = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
};
group('ユーザー一覧取得', () => {
const res = http.get(`${BASE_URL}/api/v1/users?page=1&limit=20`, { headers });
check(res, {
'ステータス200': (r) => r.status === 200,
'レスポンスタイム < 500ms': (r) => r.timings.duration < 500,
'データが返却される': (r) => r.json('data.length') > 0,
}) || errorRate.add(1);
});
group('ユーザー詳細取得', () => {
const res = http.get(`${BASE_URL}/api/v1/users/user-001`, { headers });
check(res, {
'ステータス200': (r) => r.status === 200,
'レスポンスタイム < 200ms': (r) => r.timings.duration < 200,
}) || errorRate.add(1);
});
group('検索API', () => {
const res = http.get(`${BASE_URL}/api/v1/users/search?q=テスト&page=1`, { headers });
check(res, {
'ステータス200': (r) => r.status === 200,
'レスポンスタイム < 1000ms': (r) => r.timings.duration < 1000,
}) || errorRate.add(1);
});
sleep(1);
}
CI/CDパイプラインへの統合
GitHub Actionsでの自動テスト実行
# .github/workflows/api-tests.yml
name: API Tests
on:
pull_request:
paths:
- 'src/api/**'
- 'tests/api/**'
- 'openapi.yaml'
jobs:
api-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx prisma migrate deploy
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test
- name: Run API tests
run: npm run test:api -- --coverage
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
コントラクトテストの自動化
マイクロサービス環境では、サービス間のAPIコントラクトを自動検証することが重要です。
gemini "OpenAPI仕様をもとに、
コンシューマー駆動コントラクトテスト(Pact)の
テストケースを生成して。
以下のサービス間連携をカバー:
- 注文サービス → ユーザーサービス
- 注文サービス → 在庫サービス
- 通知サービス → ユーザーサービス"
// tests/contract/order-user.pact.test.ts
import { Pact } from '@pact-foundation/pact';
import { OrderService } from '../../src/services/order.service';
const provider = new Pact({
consumer: 'OrderService',
provider: 'UserService',
port: 1234,
log: './logs/pact.log',
dir: './pacts',
});
describe('注文サービス → ユーザーサービス コントラクト', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
it('ユーザー情報を取得できる', async () => {
await provider.addInteraction({
state: 'ユーザーID user-001 が存在する',
uponReceiving: 'ユーザー情報取得リクエスト',
withRequest: {
method: 'GET',
path: '/api/v1/users/user-001',
headers: {
Authorization: 'Bearer valid-token',
},
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
data: {
id: 'user-001',
name: expect.any(String),
email: expect.any(String),
shippingAddress: {
postalCode: expect.stringMatching(/^\d{3}-\d{4}$/),
prefecture: expect.any(String),
city: expect.any(String),
address: expect.any(String),
},
},
},
},
});
const service = new OrderService(`http://localhost:1234`);
const user = await service.getUserInfo('user-001');
expect(user.data.id).toBe('user-001');
expect(user.data.shippingAddress).toBeDefined();
});
});
テスト結果のモニタリングと分析
テスト品質ダッシュボード
APIテストの品質を継続的にモニタリングするダッシュボードを構築しましょう。
gemini "APIテスト結果のJSONレポートを分析して、
以下のメトリクスをまとめて:
- エンドポイント別の成功率
- 平均レスポンスタイム
- 最も遅いエンドポイントTOP5
- Flakyテストの検出
- カバレッジの推移"
モニタリング指標:
| 指標 | 目標値 | アラート閾値 |
|---|---|---|
| テスト成功率 | 99.9% | < 98% |
| 平均レスポンスタイム | < 200ms | > 500ms |
| p99レスポンスタイム | < 1000ms | > 2000ms |
| Flakyテスト率 | 0% | > 2% |
| APIカバレッジ | > 90% | < 80% |
SES現場での実践パターン
パターン1: 既存APIのテスト追加
テストのない既存APIにテストを追加するアプローチです。
gemini "src/routes/ディレクトリの全APIエンドポイントを走査して、
テストが不足しているエンドポイントをリストアップ。
優先度順(ビジネスクリティカル → 複雑度高 → よく使われる)
でテスト追加計画を立てて"
パターン2: API仕様変更のリグレッション検知
gemini "openapi.yamlの変更差分を分析して、
破壊的変更がないか検証。
影響を受けるテストケースをリストアップして、
必要な修正を提案して"
パターン3: マイクロサービスの統合テスト
gemini "docker-compose.test.ymlを使って、
全マイクロサービスを起動した状態での
統合テストシナリオを生成して。
サービス間の依存関係を分析して、
テスト実行順序を最適化して"
まとめ|Gemini CLIでAPIテストを完全自動化
Gemini CLIを活用したAPIテスト自動化のポイントをまとめます。
導入ステップ:
- OpenAPI/GraphQLスキーマを整備 - テスト自動生成の基盤
- 基本CRUDテストの自動生成 - 正常系・異常系を網羅
- 認証フローのテスト整備 - セキュリティの基盤
- CI/CDパイプラインへの組み込み - 継続的な品質保証
- 負荷テストの追加 - パフォーマンス要件の検証
- コントラクトテストの導入 - マイクロサービスの信頼性
期待できる効果:
- APIテスト作成工数: 70%削減
- テストカバレッジ: 90%以上を維持
- バグの早期発見率: 60%向上
- API仕様変更時の影響分析: 即座に完了
SES現場でのAPIテスト自動化は、品質と生産性の両面で大きなインパクトがあります。Gemini CLIを活用して、効率的なAPIテスト体制を構築しましょう。