关于 route-tester
面向跨后端框架测试 HTTP API 路由的通用指南,适用于相关开发者。内容涵盖单元、集成和端到端测试,以及认证、响应校验、错误处理和测试隔离模式。
主要功能
- 01单元、集成与端到端测试
- 02Cookie 与 Bearer Token 认证测试
- 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 路由和端点。
核心测试原则
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 令牌身份验证
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