mcpField
R
route-tester

route-tester

github.com/blencorpテスト・品質11094 回インストール48 週間前に更新
リポジトリ

route-tester について

複数のバックエンドフレームワークで HTTP API ルートをテストする開発者向けの汎用スキルです。単体・統合・エンドツーエンドテストに加え、認証、レスポンス検証、エラー処理、テストの分離を扱います。

主な機能

  1. 01単体・統合・エンドツーエンドテスト
  2. 02Cookie とベアラートークンの認証テスト
  3. 03HTTP メソッドとステータスの確認
  4. 04レスポンススキーマの検証
  5. 05エラー処理のテストパターン
  6. 06テストの初期化と後処理

活用シーン

  1. 01保護された API ルートのテスト
  2. 02レスポンスとステータスコードの検証
  3. 03分離された統合テストの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/blencorp/claude-code-kit --skill route-tester

インストール後、Claude Code で/route-testerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

skill-rules-fragment.json
resources/
api-integration-testing.md
authentication-testing.md
http-testing-fundamentals.md

詳細

API ルートテストスキル

このスキルでは、あらゆるバックエンドフレームワーク(Express、Next.js API Routes、FastAPI、Django REST、Flask など)で HTTP API のルートとエンドポイントをテストするための、フレームワークに依存しないガイダンスを提供します。

API ルートのテストに関する基本原則

1. API ルートのテストの種類

ユニットテスト

  • 個々のルートハンドラーを切り離してテストします
  • 依存関係(データベース、外部 API)をモックします
  • 実行が高速です(テストあたり 50 ms 未満)
  • ビジネスロジックに焦点を当てます

統合テスト

  • リクエストとレスポンスの一連の流れをテストします
  • 実際のデータベース(テスト用インスタンス)を使います
  • 認証フローを含みます
  • 実行は遅くなりますが、より包括的です

エンドツーエンドテスト

  • クライアントの視点からテストします
  • 認証フロー全体を含みます
  • 実際のサービス(またはそれに近い複製)を使います
  • 最も現実に近い一方、実行に最も時間がかかります
2. 認証テストのパターン
JWT Cookie 認証
// Common pattern across frameworks
describe('Protected Route Tests', () => {
  let authCookie: string;

  beforeEach(async () => {
    // Login and get JWT cookie
    const loginResponse = await request(app)
      .post('/api/auth/login')
      .send({ email: '[email protected]', password: 'password123' });

    authCookie = loginResponse.headers['set-cookie'][0];
  });

  it('should access protected route with valid cookie', async () => {
    const response = await request(app)
      .get('/api/protected/resource')
      .set('Cookie', authCookie);

    expect(response.status).toBe(200);
  });

  it('should reject access without cookie', async () => {
    const response = await request(app)
      .get('/api/protected/resource');

    expect(response.status).toBe(401);
  });
});
JWT Bearer Token 認証
describe('Bearer Token Auth', () => {
  let token: string;

  beforeEach(async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ email: '[email protected]', password: 'password123' });

    token = response.body.token;
  });

  it('should authenticate with bearer token', async () => {
    const response = await request(app)
      .get('/api/protected/resource')
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(200);
  });
});
3. HTTP メソッドのテスト

GET リクエスト

describe('GET /api/users', () => {
  it('should return paginated users', async () => {
    const response = await request(app)
      .get('/api/users?page=1&limit=10');

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('data');
    expect(response.body).toHaveProperty('pagination');
    expect(Array.isArray(response.body.data)).toBe(true);
  });

  it('should filter users by query params', async () => {
    const response = await request(app)
      .get('/api/users?role=admin');

    expect(response.status).toBe(200);
    expect(response.body.data.every(u => u.role === 'admin')).toBe(true);
  });
});

POST リクエスト

describe('POST /api/users', () => {
  it('should create new user with valid data', async () => {
    const newUser = {
      name: 'John Doe',
      email: '[email protected]',
      role: 'user'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Cookie', authCookie)
      .send(newUser);

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject(newUser);
    expect(response.body).toHaveProperty('id');
  });

  it('should reject invalid data', async () => {
    const invalidUser = {
      name: 'John Doe'
      // Missing required email field
    };

    const response = await request(app)
      .post('/api/users')
      .set('Cookie', authCookie)
      .send(invalidUser);

    expect(response.status).toBe(400);
    expect(response.body).toHaveProperty('errors');
  });
});

PUT/PATCH リクエスト

describe('PATCH /api/users/:id', () => {
  it('should update user fields', async () => {
    const updates = { name: 'Jane Doe' };

    const response = await request(app)
      .patch('/api/users/123')
      .set('Cookie', authCookie)
      .send(updates);

    expect(response.status).toBe(200);
    expect(response.body.name).toBe('Jane Doe');
  });

  it('should return 404 for non-existent user', async () => {
    const response = await request(app)
      .patch('/api/users/999999')
      .set('Cookie', authCookie)
      .send({ name: 'Test' });

    expect(response.status).toBe(404);
  });
});

DELETE リクエスト

describe('DELETE /api/users/:id', () => {
  it('should delete user and return success', async () => {
    const response = await request(app)
      .delete('/api/users/123')
      .set('Cookie', authCookie);

    expect(response.status).toBe(204);
  });

  it('should prevent unauthorized deletion', async () => {
    const response = await request(app)
      .delete('/api/users/123');
      // No auth cookie

    expect(response.status).toBe(401);
  });
});
4. レスポンスの検証

ステータスコード

describe('HTTP Status Codes', () => {
  it('200 OK - Successful GET', async () => {
    const response = await request(app).get('/api/users');
    expect(response.status).toBe(200);
  });

  it('201 Created - Successful POST', async () => {
    const response = await request(app).post('/api/users').send(validData);
    expect(response.status).toBe(201);
  });

  it('204 No Content - Successful DELETE', async () => {
    const response = await request(app).delete('/api/users/123');
    expect(response.status).toBe(204);
  });

  it('400 Bad Request - Invalid input', async () => {
    const response = await request(app).post('/api/users').send({});
    expect(response.status).toBe(400);
  });

  it('401 Unauthorized - Missing auth', async () => {
    const response = await request(app).get('/api/protected');
    expect(response.status).toBe(401);
  });

  it('403 Forbidden - Insufficient permissions', async () => {
    const response = await request(app).delete('/api/admin/users/123').set('Cookie', userCookie);
    expect(response.status).toBe(403);
  });

  it('404 Not Found - Non-existent resource', async () => {
    const response = await request(app).get('/api/users/999999');
    expect(response.status).toBe(404);
  });

  it('500 Internal Server Error - Server failure', async () => {
    // Test error handling
    mockDatabase.findOne.mockRejectedValue(new Error('DB Error'));
    const response = await request(app).get('/api/users/123');
    expect(response.status).toBe(500);
  });
});

レスポンススキーマの検証

describe('Response Schema', () => {
  it('should match expected schema', async () => {
    const response = await request(app).get('/api/users/123');

    expect(response.body).toEqual({
      id: expect.any(String),
      name: expect.any(String),
      email: expect.any(String),
      role: expect.stringMatching(/^(user|admin)$/),
      createdAt: expect.any(String),
      updatedAt: expect.any(String)
    });
  });
});
5. エラーハンドリングのテスト
describe('Error Handling', () => {
  it('should return structured error response', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ invalid: 'data' });

    expect(response.status).toBe(400);
    expect(response.body).toEqual({
      error: expect.any(String),
      message: expect.any(String),
      errors: expect.any(Array)
    });
  });

  it('should handle database errors gracefully', async () => {
    mockDatabase.findOne.mockRejectedValue(new Error('Connection lost'));

    const response = await request(app).get('/api/users/123');

    expect(response.status).toBe(500);
    expect(response.body.error).toBe('Internal Server Error');
  });

  it('should sanitize error messages in production', async () => {
    process.env.NODE_ENV = 'production';

    const response = await request(app).get('/api/error-prone-route');

    expect(response.status).toBe(500);
    expect(response.body.message).not.toContain('stack trace');
    expect(response.body.message).not.toContain('SQL');
  });
});
6. テストのセットアップと後処理
describe('API Tests', () => {
  let testDatabase;

  beforeAll(async () => {
    // Initialize test database
    testDatabase = await initTestDatabase();
  });

  afterAll(async () => {
    // Clean up test database
    await testDatabase.close();
  });

  beforeEach(async () => {
    // Seed test data
    await testDatabase.seed();
  });

  afterEach(async () => {
    // Clear test data
    await testDatabase.clear();
  });

  // Tests...
});

フレームワーク別のテストライブラリ

このスキルではフレームワークに依存しないパターンを紹介しますが、各フレームワークでよく使われるテストライブラリは以下のとおりです。

  • Express: supertest、jest、vitest
  • Next.js API Routes: @testing-library/react、next-test-api-route-handler
  • FastAPI: pytest、httpx
  • Django REST: django.test.TestCase、rest_framework.test
  • Flask: pytest、flask.testing

ベストプラクティス

  1. 説明的なテスト名を使う - テスト名にはシナリオと期待される結果を記述します
  2. 正常系とエッジケースをテストする - 成功と失敗の両方のシナリオを網羅します
  3. テストを分離する - 各テストは独立させ、ほかのテストに依存しないようにします
  4. 現実的なテストデータを使う - テストデータは本番データを模したものにします
  5. テスト後にクリーンアップする - テストの間に必ず状態をリセットします
  6. 外部依存関係をモックする - テストでは実際の外部 API を呼び出さないでください
  7. 認証のエッジケースをテストする - 期限切れのトークン、無効なトークン、トークンがない場合をテストします
  8. レスポンススキーマを検証する - API が期待どおりの構造を返すことを確認します
  9. レート制限をテストする - レート制限が正しく機能することを確認します
  10. CORS ヘッダーをテストする - CORS が正しく設定されていることを確認します

よくある落とし穴

❌ テスト間で状態を共有しない

// Bad
let userId;
it('creates user', async () => {
  const response = await request(app).post('/api/users').send(userData);
  userId = response.body.id; // Shared state!
});

it('deletes user', async () => {
  await request(app).delete(`/api/users/${userId}`); // Depends on previous test
});

✅ 各テストに新しい状態を作成する

// Good
it('creates user', async () => {
  const response = await request(app).post('/api/users').send(userData);
  expect(response.status).toBe(201);
});

it('deletes user', async () => {
  const user = await createTestUser();
  const response = await request(app).delete(`/api/users/${user.id}`);
  expect(response.status).toBe(204);
});

参考資料

より詳しいガイドについては、resources/ ディレクトリを参照してください。

  • http-testing-fundamentals.md - HTTP テストの概念を詳しく解説
  • authentication-testing.md - 認証戦略とエッジケース
  • api-integration-testing.md - 統合テストのパターンとツール

クイックリファレンス

テストの構成

describe('Resource Name', () => {
  describe('HTTP Method /path', () => {
    it('should describe expected behavior', async () => {
      // Arrange
      const testData = {...};

      // Act
      const response = await request(app)
        .method('/path')
        .set('Cookie', authCookie)
        .send(testData);

      // Assert
      expect(response.status).toBe(expectedStatus);
      expect(response.body).toMatchObject(expectedData);
    });
  });
});

認証パターン

let authCookie: string;

beforeEach(async () => {
  const response = await request(app)
    .post('/api/auth/login')
    .send({ email: '[email protected]', password: 'password123' });

  authCookie = response.headers['set-cookie'][0];
});

// Use authCookie in protected route tests
.set('Cookie', authCookie)

コメント