|
@@ -1,364 +1,420 @@
|
|
|
-# 集成测试最佳实践
|
|
|
|
|
|
|
+# 集成测试最佳实践指南
|
|
|
|
|
|
|
|
## 概述
|
|
## 概述
|
|
|
|
|
|
|
|
-本文档提供了在项目中编写和维护集成测试的最佳实践指南。集成测试用于验证多个组件或服务之间的协作是否正确。
|
|
|
|
|
|
|
+本文档提供了项目集成测试的最佳实践、模式和指南,帮助开发团队编写高质量、可维护的集成测试。
|
|
|
|
|
|
|
|
## 测试类型区分
|
|
## 测试类型区分
|
|
|
|
|
|
|
|
-### 单元测试 vs 集成测试
|
|
|
|
|
|
|
+### 单元测试 (Unit Tests)
|
|
|
|
|
+- **范围**: 单个函数、类或组件
|
|
|
|
|
+- **目标**: 验证独立单元的 correctness
|
|
|
|
|
+- **位置**: `src/**/__tests__/**/*.test.{ts,tsx}`
|
|
|
|
|
|
|
|
-| 测试类型 | 测试范围 | 使用场景 |
|
|
|
|
|
-|---------|---------|---------|
|
|
|
|
|
-| **单元测试** | 单个函数、方法、类 | 验证独立逻辑的正确性 |
|
|
|
|
|
-| **集成测试** | 多个组件、服务协作 | 验证系统各部分集成是否正确 |
|
|
|
|
|
-| **端到端测试** | 完整用户流程 | 验证从用户界面到后端的完整流程 |
|
|
|
|
|
|
|
+### 集成测试 (Integration Tests)
|
|
|
|
|
+- **范围**: 多个组件/服务协作
|
|
|
|
|
+- **目标**: 验证模块间集成和交互
|
|
|
|
|
+- **位置**: `src/**/__integration_tests__/**/*.integration.test.{ts,tsx}`
|
|
|
|
|
|
|
|
-## API集成测试模式
|
|
|
|
|
|
|
+### E2E测试 (End-to-End Tests)
|
|
|
|
|
+- **范围**: 完整用户流程
|
|
|
|
|
+- **目标**: 验证端到端业务流程
|
|
|
|
|
+- **位置**: `tests/e2e/**/*.test.{ts,tsx}`
|
|
|
|
|
|
|
|
-### 基本测试结构
|
|
|
|
|
|
|
+## API 集成测试模式
|
|
|
|
|
|
|
|
|
|
+### 基本结构
|
|
|
```typescript
|
|
```typescript
|
|
|
-describe('API Endpoint Integration', () => {
|
|
|
|
|
|
|
+describe('API Integration Tests', () => {
|
|
|
let app: Hono;
|
|
let app: Hono;
|
|
|
let apiClient: ApiClient;
|
|
let apiClient: ApiClient;
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
beforeEach(async () => {
|
|
|
// 设置测试环境
|
|
// 设置测试环境
|
|
|
- vi.clearAllMocks();
|
|
|
|
|
-
|
|
|
|
|
- // 创建测试应用
|
|
|
|
|
- app = new Hono();
|
|
|
|
|
-
|
|
|
|
|
- // 注册路由
|
|
|
|
|
- const routes = await import('./routes');
|
|
|
|
|
- routes.default(app);
|
|
|
|
|
-
|
|
|
|
|
- // 创建API客户端
|
|
|
|
|
|
|
+ app = createTestApp();
|
|
|
apiClient = createApiClient(app);
|
|
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();
|
|
|
|
|
|
|
+ afterEach(() => {
|
|
|
|
|
+ // 清理资源
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 认证和授权测试
|
|
|
|
|
-
|
|
|
|
|
|
|
+### 请求测试模式
|
|
|
```typescript
|
|
```typescript
|
|
|
-describe('Authentication', () => {
|
|
|
|
|
- it('should require authentication', async () => {
|
|
|
|
|
- const response = await apiClient.get('/protected', {}, { authToken: undefined });
|
|
|
|
|
- expect(response.status).toBe(401);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+it('应该返回正确的状态码和数据', async () => {
|
|
|
|
|
+ const response = await apiClient.get('/api/endpoint');
|
|
|
|
|
|
|
|
- it('should require specific roles', async () => {
|
|
|
|
|
- // Mock用户具有不同角色
|
|
|
|
|
- const response = await apiClient.get('/admin-only');
|
|
|
|
|
- expect(response.status).toBe(403);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ expect(response.status).toBe(200);
|
|
|
|
|
+ expect(response.data).toMatchObject(expectedData);
|
|
|
});
|
|
});
|
|
|
-```
|
|
|
|
|
|
|
|
|
|
-### 错误处理测试
|
|
|
|
|
|
|
+it('应该处理错误情况', async () => {
|
|
|
|
|
+ const response = await apiClient.get('/api/invalid-endpoint');
|
|
|
|
|
|
|
|
-```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');
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ expect(response.status).toBe(404);
|
|
|
|
|
+ expect(response.data).toHaveProperty('error');
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## React组件集成测试模式
|
|
|
|
|
-
|
|
|
|
|
-### 组件渲染测试
|
|
|
|
|
-
|
|
|
|
|
|
|
+### 认证测试模式
|
|
|
```typescript
|
|
```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('需要认证的端点应该验证令牌', async () => {
|
|
|
|
|
+ apiClient.clearAuthToken();
|
|
|
|
|
|
|
|
- it('should handle user interactions', async () => {
|
|
|
|
|
- const user = userEvent.setup();
|
|
|
|
|
- const handleClick = vi.fn();
|
|
|
|
|
|
|
+ const response = await apiClient.get('/api/protected');
|
|
|
|
|
+ expect(response.status).toBe(401);
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
- render(
|
|
|
|
|
- <TestWrapper>
|
|
|
|
|
- <MyComponent onClick={handleClick} />
|
|
|
|
|
- </TestWrapper>
|
|
|
|
|
- );
|
|
|
|
|
|
|
+it('有效令牌应该允许访问', async () => {
|
|
|
|
|
+ apiClient.setAuthToken('valid-token');
|
|
|
|
|
|
|
|
- await user.click(screen.getByRole('button'));
|
|
|
|
|
- expect(handleClick).toHaveBeenCalledTimes(1);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const response = await apiClient.get('/api/protected');
|
|
|
|
|
+ expect(response.status).toBe(200);
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 路由和导航测试
|
|
|
|
|
|
|
+## React 组件集成测试模式
|
|
|
|
|
|
|
|
|
|
+### 基本渲染测试
|
|
|
```typescript
|
|
```typescript
|
|
|
-describe('Routing', () => {
|
|
|
|
|
- it('should navigate to correct route', async () => {
|
|
|
|
|
- renderWithRouter(<App />, { route: '/login' });
|
|
|
|
|
|
|
+it('应该渲染所有子组件', () => {
|
|
|
|
|
+ render(
|
|
|
|
|
+ <TestWrapper>
|
|
|
|
|
+ <ComplexComponent />
|
|
|
|
|
+ </TestWrapper>
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ expect(screen.getByText('Expected Text')).toBeInTheDocument();
|
|
|
|
|
+ expect(screen.getByRole('button')).toBeInTheDocument();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
- expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
|
|
|
- expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+### 用户交互测试
|
|
|
|
|
+```typescript
|
|
|
|
|
+it('应该处理用户输入和提交', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
|
|
+ const onSubmit = vi.fn();
|
|
|
|
|
|
|
|
- it('should handle protected routes', () => {
|
|
|
|
|
- renderWithRouter(<App />, { route: '/admin' });
|
|
|
|
|
|
|
+ render(
|
|
|
|
|
+ <TestWrapper>
|
|
|
|
|
+ <FormComponent onSubmit={onSubmit} />
|
|
|
|
|
+ </TestWrapper>
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- // 未认证用户应该被重定向
|
|
|
|
|
- expect(screen.getByText('Redirecting to login...')).toBeInTheDocument();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('Email'), 'test@example.com');
|
|
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Submit' }));
|
|
|
|
|
+
|
|
|
|
|
+ expect(onSubmit).toHaveBeenCalledWith('test@example.com');
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 状态管理测试
|
|
|
|
|
-
|
|
|
|
|
|
|
+### 路由集成测试
|
|
|
```typescript
|
|
```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();
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
|
|
+it('应该正确处理导航', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
|
|
+
|
|
|
|
|
+ render(
|
|
|
|
|
+ <TestQueryProvider>
|
|
|
|
|
+ <TestRouter initialPath="/home">
|
|
|
|
|
+ <App />
|
|
|
|
|
+ </TestRouter>
|
|
|
|
|
+ </TestQueryProvider>
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ await user.click(screen.getByText('Go to Settings'));
|
|
|
|
|
+ expect(screen.getByText('Settings Page')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## Mock策略
|
|
|
|
|
-
|
|
|
|
|
-### 数据库Mock
|
|
|
|
|
|
|
+## Mock 策略
|
|
|
|
|
|
|
|
|
|
+### 数据库 Mock
|
|
|
```typescript
|
|
```typescript
|
|
|
// 使用内存数据库
|
|
// 使用内存数据库
|
|
|
const testDataSource = new DataSource({
|
|
const testDataSource = new DataSource({
|
|
|
type: 'better-sqlite3',
|
|
type: 'better-sqlite3',
|
|
|
database: ':memory:',
|
|
database: ':memory:',
|
|
|
|
|
+ entities: [User, Role],
|
|
|
synchronize: true,
|
|
synchronize: true,
|
|
|
- entities: [User, Role]
|
|
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// 或者使用mock repository
|
|
|
|
|
-const mockRepo = {
|
|
|
|
|
- find: vi.fn().mockResolvedValue([]),
|
|
|
|
|
- findOne: vi.fn().mockResolvedValue(null),
|
|
|
|
|
- save: vi.fn().mockResolvedValue({ id: 1 })
|
|
|
|
|
-};
|
|
|
|
|
|
|
+// 使用事务回滚确保测试隔离
|
|
|
|
|
+beforeEach(async () => {
|
|
|
|
|
+ await testDataSource.initialize();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+afterEach(async () => {
|
|
|
|
|
+ await testDataSource.destroy();
|
|
|
|
|
+});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 外部服务Mock
|
|
|
|
|
|
|
+### 服务 Mock
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 使用 vi.mock()
|
|
|
|
|
+vi.mock('../services/external-service', () => ({
|
|
|
|
|
+ ExternalService: {
|
|
|
|
|
+ fetchData: vi.fn().mockResolvedValue(mockData),
|
|
|
|
|
+ sendData: vi.fn().mockResolvedValue({ success: true }),
|
|
|
|
|
+ }
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+// 或者使用自定义mock工具
|
|
|
|
|
+import { ServiceMocks } from '../__test_utils__/service-mocks';
|
|
|
|
|
+
|
|
|
|
|
+beforeEach(() => {
|
|
|
|
|
+ ServiceMocks.setupForSuccess();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
|
|
+### HTTP 请求 Mock
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 使用MSW mock HTTP请求
|
|
|
|
|
|
|
+// 使用 MSW (Mock Service Worker)
|
|
|
import { setupServer } from 'msw/node';
|
|
import { setupServer } from 'msw/node';
|
|
|
import { rest } from 'msw';
|
|
import { rest } from 'msw';
|
|
|
|
|
|
|
|
const server = setupServer(
|
|
const server = setupServer(
|
|
|
rest.get('/api/users', (req, res, ctx) => {
|
|
rest.get('/api/users', (req, res, ctx) => {
|
|
|
- return res(ctx.json([{ id: 1, name: 'Test User' }]));
|
|
|
|
|
|
|
+ return res(ctx.json({ users: [] }));
|
|
|
})
|
|
})
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
-// 或者使用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();
|
|
|
|
|
- })
|
|
|
|
|
-}));
|
|
|
|
|
|
|
+beforeAll(() => server.listen());
|
|
|
|
|
+afterEach(() => server.resetHandlers());
|
|
|
|
|
+afterAll(() => server.close());
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
## 测试数据管理
|
|
## 测试数据管理
|
|
|
|
|
|
|
|
### 测试数据工厂
|
|
### 测试数据工厂
|
|
|
-
|
|
|
|
|
```typescript
|
|
```typescript
|
|
|
-export function createTestUser(overrides: Partial<User> = {}): User {
|
|
|
|
|
|
|
+export function createTestUser(overrides = {}): User {
|
|
|
return {
|
|
return {
|
|
|
id: 1,
|
|
id: 1,
|
|
|
username: 'testuser',
|
|
username: 'testuser',
|
|
|
email: 'test@example.com',
|
|
email: 'test@example.com',
|
|
|
- password: 'hashed_password',
|
|
|
|
|
createdAt: new Date(),
|
|
createdAt: new Date(),
|
|
|
- updatedAt: new Date(),
|
|
|
|
|
...overrides
|
|
...overrides
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 使用工厂创建测试数据
|
|
|
|
|
-const adminUser = createTestUser({ username: 'admin', roles: ['admin'] });
|
|
|
|
|
-const inactiveUser = createTestUser({ status: 'inactive' });
|
|
|
|
|
|
|
+// 使用
|
|
|
|
|
+const adminUser = createTestUser({ role: 'admin' });
|
|
|
|
|
+const inactiveUser = createTestUser({ active: false });
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 测试数据清理
|
|
|
|
|
-
|
|
|
|
|
|
|
+### 数据清理策略
|
|
|
```typescript
|
|
```typescript
|
|
|
-afterEach(async () => {
|
|
|
|
|
- // 清理测试数据
|
|
|
|
|
- await testDataSource.getRepository(User).clear();
|
|
|
|
|
- await testDataSource.getRepository(Role).clear();
|
|
|
|
|
|
|
+// 策略1: 事务回滚 (推荐)
|
|
|
|
|
+describe('User API', () => {
|
|
|
|
|
+ let dataSource: DataSource;
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(async () => {
|
|
|
|
|
+ dataSource = await TestDatabase.initialize();
|
|
|
|
|
+ await dataSource.startTransaction();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ afterEach(async () => {
|
|
|
|
|
+ await dataSource.rollbackTransaction();
|
|
|
|
|
+ await dataSource.destroy();
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-afterAll(async () => {
|
|
|
|
|
- // 关闭数据库连接
|
|
|
|
|
- await testDataSource.destroy();
|
|
|
|
|
|
|
+// 策略2: 清理数据库
|
|
|
|
|
+afterEach(async () => {
|
|
|
|
|
+ await dataSource.getRepository(User).clear();
|
|
|
|
|
+ await dataSource.getRepository(Role).clear();
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
## 性能优化
|
|
## 性能优化
|
|
|
|
|
|
|
|
-### 测试并行化
|
|
|
|
|
-
|
|
|
|
|
|
|
+### 测试执行优化
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 在vitest.config.ts中配置
|
|
|
|
|
|
|
+// 共享测试资源
|
|
|
|
|
+let sharedDataSource: DataSource;
|
|
|
|
|
+
|
|
|
|
|
+beforeAll(async () => {
|
|
|
|
|
+ sharedDataSource = await TestDatabase.initialize();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+afterAll(async () => {
|
|
|
|
|
+ await sharedDataSource.destroy();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 使用并行测试执行
|
|
|
|
|
+// 在 vitest.config.ts 中配置
|
|
|
export default defineConfig({
|
|
export default defineConfig({
|
|
|
test: {
|
|
test: {
|
|
|
maxThreads: 4,
|
|
maxThreads: 4,
|
|
|
minThreads: 2,
|
|
minThreads: 2,
|
|
|
- fileParallelism: true
|
|
|
|
|
|
|
+ // ...
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 测试数据隔离
|
|
|
|
|
-
|
|
|
|
|
|
|
+### 减少不必要的操作
|
|
|
```typescript
|
|
```typescript
|
|
|
-// 使用事务确保测试隔离
|
|
|
|
|
-describe('User Tests', () => {
|
|
|
|
|
- let dataSource: DataSource;
|
|
|
|
|
- let queryRunner: QueryRunner;
|
|
|
|
|
|
|
+// 避免在每个测试中重新初始化
|
|
|
|
|
+beforeAll(async () => {
|
|
|
|
|
+ // 一次性初始化
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
- beforeEach(async () => {
|
|
|
|
|
- dataSource = await createTestDataSource();
|
|
|
|
|
- queryRunner = dataSource.createQueryRunner();
|
|
|
|
|
- await queryRunner.startTransaction();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+// 使用mock代替真实操作
|
|
|
|
|
+vi.mock('heavy-operation-module');
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
- afterEach(async () => {
|
|
|
|
|
- await queryRunner.rollbackTransaction();
|
|
|
|
|
- await queryRunner.release();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+## 调试技巧
|
|
|
|
|
|
|
|
- afterAll(async () => {
|
|
|
|
|
- await dataSource.destroy();
|
|
|
|
|
|
|
+### 测试调试
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 添加调试输出
|
|
|
|
|
+test('debug test', async () => {
|
|
|
|
|
+ console.log('Starting test...');
|
|
|
|
|
+ // 测试代码
|
|
|
|
|
+ console.log('Test completed');
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 使用 --test-timeout 参数增加超时时间
|
|
|
|
|
+// vitest --test-timeout=30000
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 网络请求调试
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 记录所有API请求
|
|
|
|
|
+beforeEach(() => {
|
|
|
|
|
+ vi.spyOn(global, 'fetch').mockImplementation(async (input, init) => {
|
|
|
|
|
+ console.log('API Request:', input, init);
|
|
|
|
|
+ return mockResponse;
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 常见陷阱和解决方案
|
|
|
|
|
|
|
+## 常见问题解决
|
|
|
|
|
+
|
|
|
|
|
+### 测试不稳定 (Flaky Tests)
|
|
|
|
|
+- **原因**: 异步操作时序问题
|
|
|
|
|
+- **解决**: 使用 `waitFor` 和适当的断言
|
|
|
|
|
|
|
|
-### 1. 测试相互干扰
|
|
|
|
|
-**问题**: 一个测试修改了全局状态,影响其他测试
|
|
|
|
|
-**解决方案**: 使用`beforeEach`和`afterEach`清理状态
|
|
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 错误方式
|
|
|
|
|
+expect(element).toBeInTheDocument(); // 可能尚未渲染
|
|
|
|
|
+
|
|
|
|
|
+// 正确方式
|
|
|
|
|
+await waitFor(() => {
|
|
|
|
|
+ expect(element).toBeInTheDocument();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-### 2. 测试执行缓慢
|
|
|
|
|
-**问题**: 集成测试执行时间过长
|
|
|
|
|
-**解决方案**: 使用内存数据库,mock外部服务
|
|
|
|
|
|
|
+### 内存泄漏
|
|
|
|
|
+- **原因**: 未正确清理资源
|
|
|
|
|
+- **解决**: 确保 afterEach/afterAll 中清理所有资源
|
|
|
|
|
|
|
|
-### 3. 测试不稳定
|
|
|
|
|
-**问题**: 测试有时通过,有时失败
|
|
|
|
|
-**解决方案**: 避免依赖时间、随机数,使用固定测试数据
|
|
|
|
|
|
|
+```typescript
|
|
|
|
|
+afterEach(async () => {
|
|
|
|
|
+ await cleanupTestResources();
|
|
|
|
|
+ vi.clearAllMocks();
|
|
|
|
|
+ vi.resetAllMocks();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-### 4. Mock过于复杂
|
|
|
|
|
-**问题**: Mock代码比实际代码还复杂
|
|
|
|
|
-**解决方案**: 使用工具函数简化mock创建
|
|
|
|
|
|
|
+### 测试执行缓慢
|
|
|
|
|
+- **原因**: 过多的真实数据库操作
|
|
|
|
|
+- **解决**: 使用内存数据库或更好的mock策略
|
|
|
|
|
+
|
|
|
|
|
+## CI/CD 集成
|
|
|
|
|
+
|
|
|
|
|
+### 测试配置
|
|
|
|
|
+```yaml
|
|
|
|
|
+# GitHub Actions 示例
|
|
|
|
|
+name: Integration Tests
|
|
|
|
|
+
|
|
|
|
|
+on: [push, pull_request]
|
|
|
|
|
+
|
|
|
|
|
+jobs:
|
|
|
|
|
+ test:
|
|
|
|
|
+ runs-on: ubuntu-latest
|
|
|
|
|
+
|
|
|
|
|
+ services:
|
|
|
|
|
+ mysql:
|
|
|
|
|
+ image: mysql:8.0
|
|
|
|
|
+ env:
|
|
|
|
|
+ MYSQL_ROOT_PASSWORD: root
|
|
|
|
|
+ MYSQL_DATABASE: test_db
|
|
|
|
|
+ ports:
|
|
|
|
|
+ - 3306:3306
|
|
|
|
|
+ options: >-
|
|
|
|
|
+ --health-cmd="mysqladmin ping"
|
|
|
|
|
+ --health-interval=10s
|
|
|
|
|
+ --health-timeout=5s
|
|
|
|
|
+ --health-retries=3
|
|
|
|
|
+
|
|
|
|
|
+ steps:
|
|
|
|
|
+ - uses: actions/checkout@v4
|
|
|
|
|
+ - uses: actions/setup-node@v4
|
|
|
|
|
+ with:
|
|
|
|
|
+ node-version: '20'
|
|
|
|
|
+ - run: npm ci
|
|
|
|
|
+ - run: npm run test:integration
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 测试报告
|
|
|
|
|
+```yaml
|
|
|
|
|
+# 生成测试报告
|
|
|
|
|
+- run: npm run test:integration -- --reporter=junit --outputFile=test-results.xml
|
|
|
|
|
+- uses: actions/upload-artifact@v4
|
|
|
|
|
+ with:
|
|
|
|
|
+ name: test-results
|
|
|
|
|
+ path: test-results.xml
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
## 代码质量检查
|
|
## 代码质量检查
|
|
|
|
|
|
|
|
-### 测试覆盖率目标
|
|
|
|
|
-- 行覆盖率: > 70%
|
|
|
|
|
-- 分支覆盖率: > 70%
|
|
|
|
|
-- 函数覆盖率: > 70%
|
|
|
|
|
-- 语句覆盖率: > 70%
|
|
|
|
|
|
|
+### ESLint 规则
|
|
|
|
|
+```javascript
|
|
|
|
|
+// .eslintrc.js
|
|
|
|
|
+module.exports = {
|
|
|
|
|
+ rules: {
|
|
|
|
|
+ 'testing-library/await-async-utils': 'error',
|
|
|
|
|
+ 'testing-library/no-await-sync-events': 'error',
|
|
|
|
|
+ 'testing-library/no-debugging-utils': 'warn',
|
|
|
|
|
+ 'testing-library/no-dom-import': 'error',
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-### 代码审查清单
|
|
|
|
|
-- [ ] 测试名称清晰描述预期行为
|
|
|
|
|
-- [ ] 使用恰当的断言
|
|
|
|
|
-- [ ] 包含错误场景测试
|
|
|
|
|
-- [ ] 测试数据准备充分
|
|
|
|
|
-- [ ] 测试清理逻辑完整
|
|
|
|
|
-- [ ] 没有重复的测试代码
|
|
|
|
|
-- [ ] 遵循AAA模式(Arrange-Act-Assert)
|
|
|
|
|
|
|
+### 测试覆盖率要求
|
|
|
|
|
+- **API 端点**: ≥ 80%
|
|
|
|
|
+- **关键业务逻辑**: ≥ 90%
|
|
|
|
|
+- **错误处理路径**: ≥ 70%
|
|
|
|
|
+- **整体覆盖率**: ≥ 75%
|
|
|
|
|
|
|
|
-## 工具和配置
|
|
|
|
|
|
|
+## 附录
|
|
|
|
|
|
|
|
-### 推荐工具
|
|
|
|
|
-- **测试框架**: Vitest
|
|
|
|
|
-- **API测试**: Supertest + 自定义API客户端
|
|
|
|
|
-- **组件测试**: Testing Library + Happy DOM
|
|
|
|
|
-- **HTTP Mock**: MSW (Mock Service Worker)
|
|
|
|
|
-- **数据库**: SQLite内存数据库
|
|
|
|
|
|
|
+### 有用命令
|
|
|
|
|
+```bash
|
|
|
|
|
+# 运行集成测试
|
|
|
|
|
+npm run test:integration
|
|
|
|
|
|
|
|
-### 配置文件示例
|
|
|
|
|
|
|
+# 运行特定测试文件
|
|
|
|
|
+npm run test:integration -- src/server/__integration_tests__/users.integration.test.ts
|
|
|
|
|
|
|
|
-```typescript
|
|
|
|
|
-// vitest.config.ts
|
|
|
|
|
-export default defineConfig({
|
|
|
|
|
- test: {
|
|
|
|
|
- environment: 'node',
|
|
|
|
|
- include: ['**/__integration_tests__/**/*.test.ts'],
|
|
|
|
|
- setupFiles: ['./src/test/setup.ts'],
|
|
|
|
|
- globals: true
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
|
|
+# 调试模式
|
|
|
|
|
+npm run test:integration -- --inspect-brk
|
|
|
|
|
+
|
|
|
|
|
+# 生成覆盖率报告
|
|
|
|
|
+npm run test:integration -- --coverage
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 总结
|
|
|
|
|
|
|
+### 推荐工具
|
|
|
|
|
+- **Vitest**: 测试框架
|
|
|
|
|
+- **Testing Library**: React 测试工具
|
|
|
|
|
+- **MSW**: HTTP Mock 工具
|
|
|
|
|
+- **better-sqlite3**: 内存数据库
|
|
|
|
|
+- **Docker**: 测试数据库容器
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
|
|
|
-集成测试是确保系统各部分正确协作的关键。遵循这些最佳实践可以编写出可靠、可维护的集成测试。记住测试的目标是增加信心,而不是追求100%的覆盖率。专注于测试关键业务逻辑和集成点。
|
|
|
|
|
|
|
+*最后更新: 2025-09-15*
|
|
|
|
|
+*版本: 1.0*
|