mcpField
R
route-tester

route-tester

github.com/blencorp测试与质量11094 次安装48 周前更新
代码仓库

关于 route-tester

面向跨后端框架测试 HTTP API 路由的通用指南,适用于相关开发者。内容涵盖单元、集成和端到端测试,以及认证、响应校验、错误处理和测试隔离模式。

主要功能

  1. 01单元、集成与端到端测试
  2. 02Cookie 与 Bearer Token 认证测试
  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 路由和端点。

核心测试原则

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

最佳实践

  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)

评论

支持 Markdown