|
|
@@ -0,0 +1,364 @@
|
|
|
+# 集成测试最佳实践
|
|
|
+
|
|
|
+## 概述
|
|
|
+
|
|
|
+本文档提供了在项目中编写和维护集成测试的最佳实践指南。集成测试用于验证多个组件或服务之间的协作是否正确。
|
|
|
+
|
|
|
+## 测试类型区分
|
|
|
+
|
|
|
+### 单元测试 vs 集成测试
|
|
|
+
|
|
|
+| 测试类型 | 测试范围 | 使用场景 |
|
|
|
+|---------|---------|---------|
|
|
|
+| **单元测试** | 单个函数、方法、类 | 验证独立逻辑的正确性 |
|
|
|
+| **集成测试** | 多个组件、服务协作 | 验证系统各部分集成是否正确 |
|
|
|
+| **端到端测试** | 完整用户流程 | 验证从用户界面到后端的完整流程 |
|
|
|
+
|
|
|
+## API集成测试模式
|
|
|
+
|
|
|
+### 基本测试结构
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('API Endpoint Integration', () => {
|
|
|
+ let app: Hono;
|
|
|
+ let apiClient: ApiClient;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ // 设置测试环境
|
|
|
+ vi.clearAllMocks();
|
|
|
+
|
|
|
+ // 创建测试应用
|
|
|
+ app = new Hono();
|
|
|
+
|
|
|
+ // 注册路由
|
|
|
+ const routes = await import('./routes');
|
|
|
+ routes.default(app);
|
|
|
+
|
|
|
+ // 创建API客户端
|
|
|
+ apiClient = createApiClient(app);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should return correct response', async () => {
|
|
|
+ // 设置mock
|
|
|
+ const mockService = require('./service').default;
|
|
|
+ mockService.doSomething.mockResolvedValue({ data: 'test' });
|
|
|
+
|
|
|
+ // 执行请求
|
|
|
+ const response = await apiClient.get('/endpoint');
|
|
|
+
|
|
|
+ // 验证结果
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ expect(response.data).toEqual({ data: 'test' });
|
|
|
+ expect(mockService.doSomething).toHaveBeenCalled();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 认证和授权测试
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Authentication', () => {
|
|
|
+ it('should require authentication', async () => {
|
|
|
+ const response = await apiClient.get('/protected', {}, { authToken: undefined });
|
|
|
+ expect(response.status).toBe(401);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should require specific roles', async () => {
|
|
|
+ // Mock用户具有不同角色
|
|
|
+ const response = await apiClient.get('/admin-only');
|
|
|
+ expect(response.status).toBe(403);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 错误处理测试
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Error Handling', () => {
|
|
|
+ it('should handle service errors gracefully', async () => {
|
|
|
+ const mockService = require('./service').default;
|
|
|
+ mockService.doSomething.mockRejectedValue(new Error('Service unavailable'));
|
|
|
+
|
|
|
+ const response = await apiClient.get('/endpoint');
|
|
|
+
|
|
|
+ expect(response.status).toBe(500);
|
|
|
+ expect(response.data).toMatchObject({
|
|
|
+ code: 500,
|
|
|
+ message: 'Service unavailable'
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should handle validation errors', async () => {
|
|
|
+ const response = await apiClient.post('/endpoint', { invalid: 'data' });
|
|
|
+
|
|
|
+ expect(response.status).toBe(400);
|
|
|
+ expect(response.data).toHaveProperty('error');
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## React组件集成测试模式
|
|
|
+
|
|
|
+### 组件渲染测试
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Component Integration', () => {
|
|
|
+ it('should render with correct props', () => {
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <MyComponent title="Test" onClick={mockHandler} />
|
|
|
+ </TestWrapper>
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(screen.getByText('Test')).toBeInTheDocument();
|
|
|
+ expect(screen.getByRole('button')).toBeEnabled();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should handle user interactions', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ const handleClick = vi.fn();
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <MyComponent onClick={handleClick} />
|
|
|
+ </TestWrapper>
|
|
|
+ );
|
|
|
+
|
|
|
+ await user.click(screen.getByRole('button'));
|
|
|
+ expect(handleClick).toHaveBeenCalledTimes(1);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 路由和导航测试
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('Routing', () => {
|
|
|
+ it('should navigate to correct route', async () => {
|
|
|
+ renderWithRouter(<App />, { route: '/login' });
|
|
|
+
|
|
|
+ expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
|
+ expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should handle protected routes', () => {
|
|
|
+ renderWithRouter(<App />, { route: '/admin' });
|
|
|
+
|
|
|
+ // 未认证用户应该被重定向
|
|
|
+ expect(screen.getByText('Redirecting to login...')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 状态管理测试
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('State Management', () => {
|
|
|
+ it('should update UI based on state changes', async () => {
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <UserProfile />
|
|
|
+ </TestWrapper>
|
|
|
+ );
|
|
|
+
|
|
|
+ // 初始状态
|
|
|
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
|
+
|
|
|
+ // 等待数据加载
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Mock策略
|
|
|
+
|
|
|
+### 数据库Mock
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 使用内存数据库
|
|
|
+const testDataSource = new DataSource({
|
|
|
+ type: 'better-sqlite3',
|
|
|
+ database: ':memory:',
|
|
|
+ synchronize: true,
|
|
|
+ entities: [User, Role]
|
|
|
+});
|
|
|
+
|
|
|
+// 或者使用mock repository
|
|
|
+const mockRepo = {
|
|
|
+ find: vi.fn().mockResolvedValue([]),
|
|
|
+ findOne: vi.fn().mockResolvedValue(null),
|
|
|
+ save: vi.fn().mockResolvedValue({ id: 1 })
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 外部服务Mock
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 使用MSW mock HTTP请求
|
|
|
+import { setupServer } from 'msw/node';
|
|
|
+import { rest } from 'msw';
|
|
|
+
|
|
|
+const server = setupServer(
|
|
|
+ rest.get('/api/users', (req, res, ctx) => {
|
|
|
+ return res(ctx.json([{ id: 1, name: 'Test User' }]));
|
|
|
+ })
|
|
|
+);
|
|
|
+
|
|
|
+// 或者使用vi.mock
|
|
|
+vi.mock('../external-service', () => ({
|
|
|
+ fetchData: vi.fn().mockResolvedValue({ data: 'test' })
|
|
|
+}));
|
|
|
+```
|
|
|
+
|
|
|
+### 认证Mock
|
|
|
+
|
|
|
+```typescript
|
|
|
+// Mock JWT验证
|
|
|
+vi.mock('../auth/middleware', () => ({
|
|
|
+ authMiddleware: vi.fn().mockImplementation((c, next) => {
|
|
|
+ c.set('user', { id: 1, username: 'testuser' });
|
|
|
+ return next();
|
|
|
+ })
|
|
|
+}));
|
|
|
+```
|
|
|
+
|
|
|
+## 测试数据管理
|
|
|
+
|
|
|
+### 测试数据工厂
|
|
|
+
|
|
|
+```typescript
|
|
|
+export function createTestUser(overrides: Partial<User> = {}): User {
|
|
|
+ return {
|
|
|
+ id: 1,
|
|
|
+ username: 'testuser',
|
|
|
+ email: 'test@example.com',
|
|
|
+ password: 'hashed_password',
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date(),
|
|
|
+ ...overrides
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 使用工厂创建测试数据
|
|
|
+const adminUser = createTestUser({ username: 'admin', roles: ['admin'] });
|
|
|
+const inactiveUser = createTestUser({ status: 'inactive' });
|
|
|
+```
|
|
|
+
|
|
|
+### 测试数据清理
|
|
|
+
|
|
|
+```typescript
|
|
|
+afterEach(async () => {
|
|
|
+ // 清理测试数据
|
|
|
+ await testDataSource.getRepository(User).clear();
|
|
|
+ await testDataSource.getRepository(Role).clear();
|
|
|
+});
|
|
|
+
|
|
|
+afterAll(async () => {
|
|
|
+ // 关闭数据库连接
|
|
|
+ await testDataSource.destroy();
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## 性能优化
|
|
|
+
|
|
|
+### 测试并行化
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 在vitest.config.ts中配置
|
|
|
+export default defineConfig({
|
|
|
+ test: {
|
|
|
+ maxThreads: 4,
|
|
|
+ minThreads: 2,
|
|
|
+ fileParallelism: true
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 测试数据隔离
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 使用事务确保测试隔离
|
|
|
+describe('User Tests', () => {
|
|
|
+ let dataSource: DataSource;
|
|
|
+ let queryRunner: QueryRunner;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ dataSource = await createTestDataSource();
|
|
|
+ queryRunner = dataSource.createQueryRunner();
|
|
|
+ await queryRunner.startTransaction();
|
|
|
+ });
|
|
|
+
|
|
|
+ afterEach(async () => {
|
|
|
+ await queryRunner.rollbackTransaction();
|
|
|
+ await queryRunner.release();
|
|
|
+ });
|
|
|
+
|
|
|
+ afterAll(async () => {
|
|
|
+ await dataSource.destroy();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## 常见陷阱和解决方案
|
|
|
+
|
|
|
+### 1. 测试相互干扰
|
|
|
+**问题**: 一个测试修改了全局状态,影响其他测试
|
|
|
+**解决方案**: 使用`beforeEach`和`afterEach`清理状态
|
|
|
+
|
|
|
+### 2. 测试执行缓慢
|
|
|
+**问题**: 集成测试执行时间过长
|
|
|
+**解决方案**: 使用内存数据库,mock外部服务
|
|
|
+
|
|
|
+### 3. 测试不稳定
|
|
|
+**问题**: 测试有时通过,有时失败
|
|
|
+**解决方案**: 避免依赖时间、随机数,使用固定测试数据
|
|
|
+
|
|
|
+### 4. Mock过于复杂
|
|
|
+**问题**: Mock代码比实际代码还复杂
|
|
|
+**解决方案**: 使用工具函数简化mock创建
|
|
|
+
|
|
|
+## 代码质量检查
|
|
|
+
|
|
|
+### 测试覆盖率目标
|
|
|
+- 行覆盖率: > 70%
|
|
|
+- 分支覆盖率: > 70%
|
|
|
+- 函数覆盖率: > 70%
|
|
|
+- 语句覆盖率: > 70%
|
|
|
+
|
|
|
+### 代码审查清单
|
|
|
+- [ ] 测试名称清晰描述预期行为
|
|
|
+- [ ] 使用恰当的断言
|
|
|
+- [ ] 包含错误场景测试
|
|
|
+- [ ] 测试数据准备充分
|
|
|
+- [ ] 测试清理逻辑完整
|
|
|
+- [ ] 没有重复的测试代码
|
|
|
+- [ ] 遵循AAA模式(Arrange-Act-Assert)
|
|
|
+
|
|
|
+## 工具和配置
|
|
|
+
|
|
|
+### 推荐工具
|
|
|
+- **测试框架**: Vitest
|
|
|
+- **API测试**: Supertest + 自定义API客户端
|
|
|
+- **组件测试**: Testing Library + Happy DOM
|
|
|
+- **HTTP Mock**: MSW (Mock Service Worker)
|
|
|
+- **数据库**: SQLite内存数据库
|
|
|
+
|
|
|
+### 配置文件示例
|
|
|
+
|
|
|
+```typescript
|
|
|
+// vitest.config.ts
|
|
|
+export default defineConfig({
|
|
|
+ test: {
|
|
|
+ environment: 'node',
|
|
|
+ include: ['**/__integration_tests__/**/*.test.ts'],
|
|
|
+ setupFiles: ['./src/test/setup.ts'],
|
|
|
+ globals: true
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## 总结
|
|
|
+
|
|
|
+集成测试是确保系统各部分正确协作的关键。遵循这些最佳实践可以编写出可靠、可维护的集成测试。记住测试的目标是增加信心,而不是追求100%的覆盖率。专注于测试关键业务逻辑和集成点。
|