route-tester について
複数のバックエンドフレームワークで HTTP API ルートをテストする開発者向けの汎用スキルです。単体・統合・エンドツーエンドテストに加え、認証、レスポンス検証、エラー処理、テストの分離を扱います。
主な機能
- 01単体・統合・エンドツーエンドテスト
- 02Cookie とベアラートークンの認証テスト
- 03HTTP メソッドとステータスの確認
- 04レスポンススキーマの検証
- 05エラー処理のテストパターン
- 06テストの初期化と後処理
活用シーン
- 01保護された API ルートのテスト
- 02レスポンスとステータスコードの検証
- 03分離された統合テストの設計
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ blencorp/ claude-code-kit --skill route-testerインストール後、Claude Code で/route-testerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
skill-rules-fragment.jsonresources/api-integration-testing.mdauthentication-testing.mdhttp-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
ベストプラクティス
- 説明的なテスト名を使う - テスト名にはシナリオと期待される結果を記述します
- 正常系とエッジケースをテストする - 成功と失敗の両方のシナリオを網羅します
- テストを分離する - 各テストは独立させ、ほかのテストに依存しないようにします
- 現実的なテストデータを使う - テストデータは本番データを模したものにします
- テスト後にクリーンアップする - テストの間に必ず状態をリセットします
- 外部依存関係をモックする - テストでは実際の外部 API を呼び出さないでください
- 認証のエッジケースをテストする - 期限切れのトークン、無効なトークン、トークンがない場合をテストします
- レスポンススキーマを検証する - API が期待どおりの構造を返すことを確認します
- レート制限をテストする - レート制限が正しく機能することを確認します
- 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)
コメント
Markdown 対応