𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
Gemini CLIでAPIテスト自動化|REST・GraphQLテストの実践ガイド

Gemini CLIでAPIテスト自動化|REST・GraphQLテストの実践ガイド

Gemini CLIAPIテストREST APIGraphQLテスト自動化2026年
目次

APIテストの作成・メンテナンスに追われていませんか?

  • 「エンドポイントが増えるたびにテストの更新が大変」
  • 「認証やページネーションのテストが複雑すぎる」
  • 「APIの仕様変更でテストが大量に壊れる」

**Gemini CLI(Google Antigravity)を活用すれば、APIテストの設計・生成・実行を劇的に効率化できます。**OpenAPI仕様やGraphQLスキーマからテストを自動生成し、CI/CDパイプラインに組み込むところまで、この記事で完全にカバーします。

Gemini CLI APIテスト自動化のワークフロー図解

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テスト自動化のポイントをまとめます。

導入ステップ:

  1. OpenAPI/GraphQLスキーマを整備 - テスト自動生成の基盤
  2. 基本CRUDテストの自動生成 - 正常系・異常系を網羅
  3. 認証フローのテスト整備 - セキュリティの基盤
  4. CI/CDパイプラインへの組み込み - 継続的な品質保証
  5. 負荷テストの追加 - パフォーマンス要件の検証
  6. コントラクトテストの導入 - マイクロサービスの信頼性

期待できる効果:

  • APIテスト作成工数: 70%削減
  • テストカバレッジ: 90%以上を維持
  • バグの早期発見率: 60%向上
  • API仕様変更時の影響分析: 即座に完了

SES現場でのAPIテスト自動化は、品質と生産性の両面で大きなインパクトがあります。Gemini CLIを活用して、効率的なAPIテスト体制を構築しましょう。

関連記事

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修