route-tester
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI Route Testing Skill
API路由测试实践
This skill provides framework-agnostic guidance for testing HTTP API routes and endpoints across any backend framework (Express, Next.js API Routes, FastAPI, Django REST, Flask, etc.).
本技能提供与框架无关的HTTP API路由和端点测试指导,适用于所有后端框架(Express、Next.js API Routes、FastAPI、Django REST、Flask等)。
Core Testing Principles
核心测试原则
1. Test Types for API Routes
1. API路由的测试类型
Unit Tests
- Test individual route handlers in isolation
- Mock dependencies (database, external APIs)
- Fast execution (< 50ms per test)
- Focus on business logic
Integration Tests
- Test full request/response cycle
- Real database (test instance)
- Authentication flow included
- Slower but more comprehensive
End-to-End Tests
- Test from client perspective
- Full authentication flow
- Real services (or close replicas)
- Most realistic, slowest execution
单元测试
- 孤立测试单个路由处理器
- 模拟依赖项(数据库、外部API)
- 执行速度快(每个测试< 50ms)
- 聚焦业务逻辑
集成测试
- 测试完整的请求/响应周期
- 使用真实数据库(测试实例)
- 包含认证流程
- 速度较慢但覆盖更全面
端到端测试
- 从客户端视角进行测试
- 完整的认证流程
- 使用真实服务(或高度相似的副本)
- 最贴近真实场景,但执行速度最慢
2. Authentication Testing Patterns
2. 认证测试模式
JWT Cookie Authentication
JWT Cookie认证
typescript
// 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: 'test@example.com', 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);
});
});typescript
// 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: 'test@example.com', 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 Authentication
JWT Bearer Token认证
typescript
describe('Bearer Token Auth', () => {
let token: string;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', 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);
});
});typescript
describe('Bearer Token Auth', () => {
let token: string;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', 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 Method Testing
3. HTTP方法测试
GET Requests
typescript
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 Requests
typescript
describe('POST /api/users', () => {
it('should create new user with valid data', async () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com',
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 Requests
typescript
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 Requests
typescript
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);
});
});GET请求
typescript
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请求
typescript
describe('POST /api/users', () => {
it('should create new user with valid data', async () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com',
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请求
typescript
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请求
typescript
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. Response Validation
4. 响应验证
Status Codes
typescript
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);
});
});Response Schema Validation
typescript
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)
});
});
});状态码
typescript
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);
});
});响应 Schema 验证
typescript
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. Error Handling Tests
5. 错误处理测试
typescript
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');
});
});typescript
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. Test Setup and Teardown
6. 测试初始化与清理
typescript
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...
});typescript
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...
});Framework-Specific Testing Libraries
框架特定测试库
While this skill provides framework-agnostic patterns, here are common testing libraries per framework:
- 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
虽然本技能提供与框架无关的测试模式,但以下是各框架常用的测试库:
- 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
Best Practices
最佳实践
- Use descriptive test names - Test names should describe the scenario and expected outcome
- Test happy path and edge cases - Cover both success and failure scenarios
- Isolate tests - Each test should be independent and not rely on other tests
- Use realistic test data - Test data should mimic production data
- Clean up after tests - Always reset state between tests
- Mock external dependencies - Don't call real external APIs in tests
- Test authentication edge cases - Expired tokens, invalid tokens, missing tokens
- Validate response schemas - Ensure APIs return expected structure
- Test rate limiting - Verify rate limits work correctly
- Test CORS headers - Ensure CORS is configured correctly
- 使用描述性的测试名称 - 测试名称应描述场景和预期结果
- 测试正常流程和边缘情况 - 覆盖成功和失败场景
- 隔离测试 - 每个测试应独立,不依赖其他测试
- 使用真实的测试数据 - 测试数据应模拟生产环境数据
- 测试后清理 - 测试间始终重置状态
- 模拟外部依赖 - 测试中不要调用真实的外部API
- 测试认证边缘情况 - 过期令牌、无效令牌、缺失令牌
- 验证响应Schema - 确保API返回预期结构
- 测试速率限制 - 验证速率限制是否正常工作
- 测试CORS头 - 确保CORS配置正确
Common Pitfalls
常见误区
❌ Don't share state between tests
typescript
// 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
});✅ Do create fresh state for each test
typescript
// 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);
});❌ 不要在测试间共享状态
typescript
// 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
});✅ 应为每个测试创建独立状态
typescript
// 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);
});Additional Resources
额外资源
See the directory for more detailed guides:
resources/- - Deep dive into HTTP testing concepts
http-testing-fundamentals.md - - Authentication strategies and edge cases
authentication-testing.md - - Integration testing patterns and tools
api-integration-testing.md
请查看目录获取更详细的指南:
resources/- - HTTP测试概念深度解析
http-testing-fundamentals.md - - 认证策略和边缘情况
authentication-testing.md - - 集成测试模式和工具
api-integration-testing.md
Quick Reference
快速参考
Test Structure
typescript
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);
});
});
});Authentication Pattern
typescript
let authCookie: string;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
authCookie = response.headers['set-cookie'][0];
});
// Use authCookie in protected route tests
.set('Cookie', authCookie)测试结构
typescript
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);
});
});
});认证模式
typescript
let authCookie: string;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
authCookie = response.headers['set-cookie'][0];
});
// Use authCookie in protected route tests
.set('Cookie', authCookie)