# 测试策略 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 2.9 | 2025-12-15 | 添加API模拟规范和前端组件测试策略,修正$path()方法描述与实际代码不一致问题 | James | | 2.8 | 2025-11-11 | 更新包测试结构,添加模块化包测试策略 | Winston | | 2.7 | 2025-11-09 | 更新为monorepo测试架构,清理重复测试文件 | James | | 2.6 | 2025-10-15 | 完成遗留测试文件迁移到统一的tests目录结构 | Winston | | 2.5 | 2025-10-14 | 更新测试文件位置到统一的tests目录结构 | Claude | | 2.4 | 2025-09-20 | 更新测试策略与主架构文档版本一致 | Winston | ## 概述 本文档定义了D8D Starter项目的完整测试策略,基于monorepo架构和现有的测试基础设施。测试策略遵循测试金字塔模型,确保代码质量、功能稳定性和系统可靠性。 ### 测试架构更新 (v2.8) 项目已重构为模块化包架构,测试架构相应调整为: - **基础设施包**: shared-types、shared-utils、shared-crud、shared-test-util - **业务模块包**: user-module、auth-module、file-module、geo-areas - **应用层**: server (重构后),包含模块集成测试 - **web**: Web应用,包含组件测试、集成测试和E2E测试 - **CI/CD**: 独立的工作流分别处理各包的测试 ### 包测试架构 (v2.8) 项目采用分层测试架构,每个包独立测试: - **基础设施包**: 纯单元测试,不依赖外部服务 - **业务模块包**: 单元测试 + 集成测试,验证模块功能 - **应用层**: 集成测试,验证模块间协作 - **共享测试工具**: shared-test-util 提供统一的测试基础设施 ## 测试金字塔策略 ### 单元测试 (Unit Tests) - **范围**: 单个函数、类或组件 - **目标**: 验证独立单元的correctness - **位置**: - **基础设施包**: `packages/shared-*/tests/unit/**/*.test.ts` - **业务模块包**: `packages/*-module/tests/unit/**/*.test.ts` - **server包**: `packages/server/tests/unit/**/*.test.ts` - **web应用**: `web/tests/unit/**/*.test.{ts,tsx}` - **框架**: Vitest - **覆盖率目标**: ≥ 80% - **执行频率**: 每次代码变更 ### 集成测试 (Integration Tests) - **范围**: 多个组件/服务协作 - **目标**: 验证模块间集成和交互 - **位置**: - **业务模块包**: `packages/*-module/tests/integration/**/*.test.ts` - **server包**: `packages/server/tests/integration/**/*.test.ts` (模块集成测试) - **web应用**: `web/tests/integration/**/*.test.{ts,tsx}` - **框架**: Vitest + Testing Library + hono/testing + shared-test-util - **覆盖率目标**: ≥ 60% - **执行频率**: 每次API变更 ### E2E测试 (End-to-End Tests) - **范围**: 完整用户流程 - **目标**: 验证端到端业务流程 - **位置**: `web/tests/e2e/**/*.test.{ts,tsx}` - **框架**: Playwright - **覆盖率目标**: 关键用户流程100% - **执行频率**: 每日或每次重大变更 ### 小程序测试策略 - **项目**: mini小程序 (Taro小程序) - **测试框架**: Jest (不是Vitest) - **测试位置**: `mini/tests/unit/**/*.test.{ts,tsx}` - **测试特点**: - 使用Jest测试框架,配置在`mini/jest.config.js`中 - 包含Taro小程序API的模拟 (`__mocks__/taroMock.ts`) - 组件测试使用React Testing Library - 支持TypeScript和ES6+语法 - **测试命令**: - `pnpm test` - 运行所有测试 - `pnpm test --testNamePattern "测试名称"` - 运行特定测试 - `pnpm test:coverage` - 生成覆盖率报告 ## 测试环境配置 ### 开发环境 ```typescript // vitest.config.ts - 开发环境配置 export default defineConfig({ test: { projects: [ // Node.js 环境项目 - 后端测试 { test: { include: [ 'tests/unit/server/**/*.test.{ts,js}', 'tests/integration/server/**/*.test.{ts,js}' ], // ... 其他配置 } }, // Happy DOM 环境项目 - 前端组件测试 { test: { include: [ 'tests/unit/client/**/*.test.{ts,js,tsx,jsx}', 'tests/integration/client/**/*.test.{ts,js,tsx,jsx}' ], // ... 其他配置 } } ] } }); ``` ### CI/CD环境 ```yaml # GitHub Actions 测试配置 (模块化包架构) name: Test Pipeline jobs: # 基础设施包测试 shared-packages-tests: runs-on: ubuntu-latest steps: - run: cd packages/shared-types && pnpm test - run: cd packages/shared-utils && pnpm test - run: cd packages/shared-crud && pnpm test - run: cd packages/shared-test-util && pnpm test # 业务模块包测试 business-modules-tests: runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_d8dai steps: - run: cd packages/user-module && pnpm test - run: cd packages/auth-module && pnpm test - run: cd packages/file-module && pnpm test - run: cd packages/geo-areas && pnpm test # 服务器集成测试 server-integration-tests: runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_d8dai steps: - run: cd packages/server && pnpm test # Web应用测试 web-integration-tests: runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_d8dai steps: - run: cd web && pnpm test:integration web-component-tests: runs-on: ubuntu-latest steps: - run: cd web && pnpm test:components web-e2e-tests: runs-on: ubuntu-latest steps: - run: cd web && pnpm test:e2e:chromium ``` ## 测试覆盖率标准 ### 各层覆盖率要求 | 测试类型 | 最低要求 | 目标要求 | 关键模块要求 | |----------|----------|----------|--------------| | 单元测试 | 70% | 80% | 90% | | 集成测试 | 50% | 60% | 70% | | E2E测试 | 关键流程100% | 主要流程80% | - | ### 关键模块定义 - **认证授权模块**: 必须达到90%单元测试覆盖率 - **数据库操作模块**: 必须达到85%单元测试覆盖率 - **核心业务逻辑**: 必须达到80%集成测试覆盖率 - **用户管理功能**: 必须100% E2E测试覆盖 ## 测试数据管理 ### 测试数据策略 ```typescript // 测试数据工厂模式 export function createTestUser(overrides = {}): User { return { id: 1, username: 'testuser', email: 'test@example.com', createdAt: new Date(), ...overrides }; } // 使用示例 const adminUser = createTestUser({ role: 'admin' }); const inactiveUser = createTestUser({ active: false }); ``` ### 数据库测试策略 - **单元测试**: 使用内存数据库或完全mock - **集成测试**: 使用专用测试数据库,事务回滚 - **E2E测试**: 使用接近生产环境的数据库 ### 数据清理策略 1. **事务回滚** (推荐) 2. **数据库清理** (每个测试后) 3. **测试数据隔离** (使用唯一标识符) ## API模拟规范 ### 概述 API模拟规范为管理后台UI包提供测试中的API模拟策略。虽然当前项目实践中每个UI包都有自己的客户端管理器,但为了简化测试复杂度、特别是跨UI包集成测试场景,规范要求统一模拟共享UI组件包中的`rpcClient`函数。 ### 问题背景 当前实现中,每个UI包测试文件模拟自己的客户端管理器(如`AdvertisementClientManager`、`UserClientManager`)。这种模式在单一UI包测试时可行,但在**跨UI包集成测试**时存在严重问题: **示例场景**:收货地址UI包中使用区域管理UI包的区域选择组件,两个组件分别使用各自的客户端管理器。 - 收货地址组件 → 使用`DeliveryAddressClientManager` - 区域选择组件 → 使用`AreaClientManager` - 测试时需要同时模拟两个客户端管理器,配置复杂且容易冲突 **统一模拟优势**:通过模拟`@d8d/shared-ui-components`包中的`rpcClient`函数,可以: 1. **统一控制**:所有API调用都经过同一个模拟点 2. **简化配置**:无需关心具体客户端管理器,只需配置API响应 3. **跨包支持**:天然支持多个UI包组件的集成测试 4. **维护性**:API响应配置集中管理,易于更新 ### 现有模式分析(仅供参考) 项目中的管理后台UI包当前遵循以下架构模式: 1. **客户端管理器模式**:每个UI包都有一个客户端管理器类(如`AdvertisementClientManager`、`UserClientManager`) 2. **rpcClient使用**:客户端管理器使用`@d8d/shared-ui-components`包中的`rpcClient`函数创建Hono RPC客户端 3. **API结构**:生成的客户端使用Hono风格的方法调用(如`index.$get`、`index.$post`、`:id.$put`等) **注意**:新的测试规范要求直接模拟`rpcClient`函数,而不是模拟各个客户端管理器。 ### rpcClient函数分析 `rpcClient`函数位于`@d8d/shared-ui-components`包的`src/utils/hc.ts`文件中,其核心功能是创建Hono RPC客户端: ```typescript // packages/shared-ui-components/src/utils/hc.ts export const rpcClient = >(aptBaseUrl: string): ReturnType> => { return hc(aptBaseUrl, { fetch: axiosFetch }) } ``` 该函数接收API基础URL参数,返回一个配置了axios适配器的Hono客户端实例。 ### 模拟策略 #### 1. 统一模拟rpcClient函数 在测试中,使用Vitest的`vi.mock`直接模拟`@d8d/shared-ui-components`包中的`rpcClient`函数,统一拦截所有API调用: ```typescript // 测试文件顶部 - 统一模拟rpcClient函数 import { vi } from 'vitest' import type { Hono } from 'hono' // 创建模拟的rpcClient函数 const mockRpcClient = vi.fn((aptBaseUrl: string) => { // 根据页面组件实际调用的RPC路径定义模拟端点 return { // 收货地址UI包使用的端点 index: { $get: vi.fn(), $post: vi.fn(), }, ':id': { $put: vi.fn(), $delete: vi.fn(), }, // 区域管理UI包使用的端点(跨包集成) provinces: { $get: vi.fn(), }, // 地区列表API端点 $get: vi.fn(), } }) // 模拟共享UI组件包中的rpcClient函数 vi.mock('@d8d/shared-ui-components/utils/hc', () => ({ rpcClient: mockRpcClient })) ``` #### 2. 创建模拟响应辅助函数 创建通用的模拟响应辅助函数,用于生成一致的API响应格式: ```typescript // 在测试文件中定义或从共享工具导入 const createMockResponse = (status: number, data?: any) => ({ status, ok: status >= 200 && status < 300, body: null, bodyUsed: false, statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error', headers: new Headers(), url: '', redirected: false, type: 'basic' as ResponseType, json: async () => data || {}, text: async () => '', blob: async () => new Blob(), arrayBuffer: async () => new ArrayBuffer(0), formData: async () => new FormData(), clone: function() { return this; } }); // 创建简化版响应工厂(针对常见业务数据结构) const createMockApiResponse = (data: T, success = true) => ({ success, data, timestamp: new Date().toISOString() }) const createMockErrorResponse = (message: string, code = 'ERROR') => ({ success: false, error: { code, message }, timestamp: new Date().toISOString() }) ``` #### 3. 在测试用例中配置模拟响应 在测试用例的`beforeEach`或具体测试中配置模拟响应,支持跨UI包集成: ```typescript // 跨UI包集成测试示例:收货地址UI包(包含区域选择组件) describe('收货地址管理(跨UI包集成)', () => { let mockClient: any; beforeEach(() => { vi.clearAllMocks(); // 获取模拟的rpcClient实例 mockClient = mockRpcClient('/'); // 配置收货地址API响应(收货地址UI包) mockClient.index.$get.mockResolvedValue(createMockResponse(200, { data: [ { id: 1, name: '测试地址', phone: '13800138000', provinceId: 1, cityId: 2, districtId: 3, detail: '测试街道' } ], pagination: { total: 1, page: 1, pageSize: 10 } })); mockClient.index.$post.mockResolvedValue(createMockResponse(201, { id: 2, name: '新地址' })); mockClient[':id']['$put'].mockResolvedValue(createMockResponse(200)); mockClient[':id']['$delete'].mockResolvedValue(createMockResponse(204)); // 配置区域API响应(区域管理UI包 - 跨包支持) mockClient.$get.mockResolvedValue(createMockResponse(200, { data: [ { id: 1, name: '北京市', code: '110000', level: 1 }, { id: 2, name: '朝阳区', code: '110105', level: 2, parentId: 1 }, { id: 3, name: '海淀区', code: '110108', level: 2, parentId: 1 } ] })); mockClient.provinces.$get.mockResolvedValue(createMockResponse(200, { data: [ { id: 1, name: '北京市', code: '110000' }, { id: 2, name: '上海市', code: '310000' } ] })); // 获取某个地区的子地区(如城市)通常通过查询参数实现 mockClient.$get.mockImplementation((options?: any) => { if (options?.query?.parentId === 1) { return Promise.resolve(createMockResponse(200, { data: [ { id: 2, name: '朝阳区', code: '110105', parentId: 1 }, { id: 3, name: '海淀区', code: '110108', parentId: 1 } ] })); } // 默认返回空列表 return Promise.resolve(createMockResponse(200, { data: [] })); }); }); it('应该显示收货地址列表并支持区域选择', async () => { // 测试代码:验证收货地址UI和区域选择组件都能正常工作 // 所有API调用都通过统一的mockRpcClient模拟 }); it('应该处理API错误场景', async () => { // 模拟API错误 mockClient.index.$get.mockRejectedValue(new Error('网络错误')); // 测试错误处理 }); }); ``` ### 管理后台UI包测试策略 #### 1. 模拟范围 - **统一模拟点**: 集中模拟`@d8d/shared-ui-components/utils/hc`中的`rpcClient`函数 - **HTTP方法**: 支持Hono风格的`$get`、`$post`、`$put`、`$delete`方法 - **API端点**: 支持标准端点(`index`)、参数化端点(`:id`)和属性访问端点(如`client.provinces.$get()`) - **响应格式**: 模拟完整的Response对象,包含`status`、`ok`、`json()`等方法 - **跨包支持**: 天然支持多个UI包组件的API模拟,无需分别模拟客户端管理器 #### 2. 测试设置 1. **统一模拟**: 在每个测试文件顶部使用`vi.mock`统一模拟`rpcClient`函数 2. **测试隔离**: 每个测试用例使用独立的模拟实例,在`beforeEach`中重置 3. **响应配置**: 根据测试场景配置不同的模拟响应(成功、失败、错误等) 4. **错误测试**: 模拟各种错误场景(网络错误、验证错误、权限错误、服务器错误等) 5. **跨包集成**: 支持配置多个UI包的API响应,适用于组件集成测试 #### 3. 最佳实践 - **统一模拟**: 所有API调用都通过模拟`rpcClient`函数统一拦截 - **按需定义**: 根据页面组件实际调用的RPC路径定义模拟端点,无需动态创建所有可能端点 - **类型安全**: 使用TypeScript确保模拟响应与API类型兼容 - **可维护性**: 保持模拟响应与实际API响应结构一致,便于后续更新 - **文档化**: 在测试注释中说明模拟的API行为和预期结果 - **响应工厂**: 创建可重用的模拟响应工厂函数,确保响应格式一致性 - **跨包考虑**: 为集成的UI包组件配置相应的API响应 ### 验证和调试 #### 1. 模拟验证 ```typescript // 验证API调用次数和参数 - 使用统一模拟的rpcClient describe('API调用验证(统一模拟)', () => { beforeEach(() => { vi.clearAllMocks(); }); it('应该验证API调用次数和参数', async () => { // 获取模拟的客户端实例 const mockClient = mockRpcClient('/'); // 配置模拟响应 mockClient.index.$get.mockResolvedValue(createMockResponse(200, { data: [] })); mockClient.index.$post.mockResolvedValue(createMockResponse(201, { id: 1 })); mockClient[':id']['$put'].mockResolvedValue(createMockResponse(200)); mockClient.$get.mockResolvedValue(createMockResponse(200, { data: [] })); // 执行测试代码(触发API调用)... // 验证API调用次数 expect(mockClient.index.$get).toHaveBeenCalledTimes(1); expect(mockClient.$get).toHaveBeenCalledTimes(1); // 验证API调用参数 expect(mockClient.index.$post).toHaveBeenCalledWith({ json: { title: '新广告', code: 'new-ad', typeId: 1 } }); // 验证带参数的API调用 expect(mockClient[':id']['$put']).toHaveBeenCalledWith({ param: { id: 1 }, json: { title: '更新后的广告' } }); // 验证地区API调用 expect(mockClient.$get).toHaveBeenCalledWith({ query: { level: 1 } }); }); it('应该验证错误场景', async () => { const mockClient = mockRpcClient('/'); // 配置错误响应 mockClient.index.$get.mockRejectedValue(new Error('网络错误')); // 执行测试代码... // 验证错误调用 expect(mockClient.index.$get).toHaveBeenCalledTimes(1); }); }); ``` #### 2. 调试技巧 - **console.debug**: 在测试中使用`console.debug`输出模拟调用信息,便于调试 ```typescript // 在测试中输出调试信息 console.debug('Mock client calls:', { getCalls: mockClient.index.$get.mock.calls, postCalls: mockClient.index.$post.mock.calls }); ``` - **调用检查**: 使用`vi.mocked()`检查模拟函数的调用参数和次数 ```typescript // 检查mockRpcClient的调用 const mockCalls = vi.mocked(mockRpcClient).mock.calls; console.debug('rpcClient调用参数:', mockCalls); // 检查具体端点调用 const getCalls = vi.mocked(mockClient.index.$get).mock.calls; ``` - **响应验证**: 确保模拟响应的格式与实际API响应一致 ```typescript // 验证响应格式 const response = await mockClient.index.$get(); expect(response.status).toBe(200); expect(response.ok).toBe(true); const data = await response.json(); expect(data).toHaveProperty('data'); expect(data).toHaveProperty('pagination'); ``` - **错误模拟**: 测试各种错误场景,确保UI能正确处理 ```typescript // 模拟不同类型的错误 mockClient.index.$get.mockRejectedValue(new Error('网络错误')); // 网络错误 mockClient.index.$get.mockResolvedValue(createMockResponse(500)); // 服务器错误 mockClient.index.$get.mockResolvedValue(createMockResponse(401)); // 认证错误 ``` - **快照测试**: 使用Vitest的快照测试验证UI在不同API响应下的渲染结果 - **跨包调试**: 在跨UI包集成测试中,验证所有相关API都正确配置了模拟响应 ## 测试执行流程 ### 本地开发测试 #### 基础设施包 ```bash # 运行所有基础设施包测试 cd packages/shared-types && pnpm test cd packages/shared-utils && pnpm test cd packages/shared-crud && pnpm test cd packages/shared-test-util && pnpm test # 生成覆盖率报告 cd packages/shared-utils && pnpm test:coverage ``` #### 业务模块包 ```bash # 运行所有业务模块包测试 cd packages/user-module && pnpm test cd packages/auth-module && pnpm test cd packages/file-module && pnpm test cd packages/geo-areas && pnpm test # 运行单元测试 cd packages/user-module && pnpm test:unit # 运行集成测试 cd packages/auth-module && pnpm test:integration # 生成覆盖率报告 cd packages/user-module && pnpm test:coverage ``` #### server包 ```bash # 运行所有测试 cd packages/server && pnpm test # 运行集成测试 cd packages/server && pnpm test:integration # 生成覆盖率报告 cd packages/server && pnpm test:coverage ``` #### web应用 ```bash # 运行所有测试 cd web && pnpm test # 运行单元测试 cd web && pnpm test:unit # 运行集成测试 cd web && pnpm test:integration # 运行组件测试 cd web && pnpm test:components # 运行E2E测试 cd web && pnpm test:e2e:chromium # 生成覆盖率报告 cd web && pnpm test:coverage ``` #### 小程序 (mini) ```bash # 运行所有测试 cd mini && pnpm test # 运行特定测试 cd mini && pnpm test --testNamePattern "商品卡片" # 生成覆盖率报告 cd mini && pnpm test:coverage # 调试测试 cd mini && pnpm test --testNamePattern "测试名称" --verbose ``` ### CI/CD流水线测试 1. **代码推送** → 触发测试流水线 2. **单元测试** → 快速反馈,必须通过 3. **集成测试** → 验证模块集成,必须通过 4. **E2E测试** → 验证完整流程,建议通过 5. **覆盖率检查** → 满足最低要求 6. **测试报告** → 生成详细报告 ## 质量门禁 ### 测试通过标准 - ✅ 所有单元测试通过 - ✅ 所有集成测试通过 - ✅ 关键E2E测试通过 - ✅ 覆盖率满足最低要求 - ✅ 无性能回归 - ✅ 安全测试通过 ### 失败处理流程 1. **测试失败** → 立即通知开发团队 2. **分析根本原因** → 确定是测试问题还是代码问题 3. **优先修复** → 阻塞性问题必须立即修复 4. **重新测试** → 修复后重新运行测试 5. **文档更新** → 更新测试策略和案例 ## 安全测试策略 ### 安全测试要求 - **输入验证测试**: 所有API端点必须测试SQL注入、XSS等攻击 - **认证测试**: 测试令牌验证、权限控制 - **数据保护**: 测试敏感数据泄露风险 - **错误处理**: 测试错误信息是否泄露敏感数据 ### 安全测试工具 - **OWASP ZAP**: 自动化安全扫描 - **npm audit**: 依赖漏洞检查 - **自定义安全测试**: 针对业务逻辑的安全测试 ## 性能测试策略 ### 性能测试要求 - **API响应时间**: < 100ms (p95) - **数据库查询性能**: < 50ms (p95) - **并发用户数**: 支持100+并发用户 - **资源使用**: CPU < 70%, 内存 < 80% ### 性能测试工具 - **k6**: 负载测试 - **autocannon**: API性能测试 - **Playwright**: E2E性能监控 ## 测试文档标准 ### 测试代码规范 ```typescript // 良好的测试示例 describe('UserService', () => { describe('createUser()', () => { it('应该创建新用户并返回用户对象', async () => { // Arrange const userData = { username: 'testuser', email: 'test@example.com' }; // Act const result = await userService.createUser(userData); // Assert expect(result).toHaveProperty('id'); expect(result.username).toBe('testuser'); expect(result.email).toBe('test@example.com'); }); it('应该拒绝重复的用户名', async () => { // Arrange const existingUser = await createTestUser({ username: 'existing' }); // Act & Assert await expect( userService.createUser({ username: 'existing', email: 'new@example.com' }) ).rejects.toThrow('用户名已存在'); }); }); }); ``` ### 测试命名约定 - **文件名**: `[module].test.ts` 或 `[module].integration.test.ts` - **描述**: 使用「应该...」格式描述测试行为 - **用例**: 明确描述测试场景和预期结果 ## 监控和报告 ### 测试监控指标 - **测试通过率**: > 95% - **测试执行时间**: < 10分钟(单元+集成) - **测试稳定性**: 无flaky tests - **覆盖率趋势**: 持续改进或保持 ### 测试报告要求 - **HTML报告**: 详细的覆盖率报告 - **JUnit报告**: CI/CD集成 - **自定义报告**: 业务指标测试报告 - **历史趋势**: 测试质量趋势分析 ## 附录 ### 相关文档 - [集成测试最佳实践](../integration-testing-best-practices.md) - [编码标准](./coding-standards.md) - [API设计规范](./api-design-integration.md) ### 工具版本 - **Vitest**: 3.2.4 - **Testing Library**: 16.3.0 - **Playwright**: 1.55.0 - **hono/testing**: 内置(Hono 4.8.5) - **shared-test-util**: 1.0.0 (测试基础设施包) - **TypeORM**: 0.3.20 (数据库测试) - **Redis**: 7.0.0 (会话管理测试) - **Jest**: 29.x (mini小程序专用) ### 更新日志 | 日期 | 版本 | 描述 | |------|------|------| | 2025-11-11 | 2.8 | 更新包测试结构,添加模块化包测试策略 | | 2025-11-09 | 2.7 | 更新为monorepo测试架构,清理重复测试文件 | | 2025-10-15 | 2.6 | 完成遗留测试文件迁移到统一的tests目录结构 | | 2025-10-14 | 2.5 | 重构测试文件结构,统一到tests目录 | | 2025-09-20 | 2.4 | 更新版本与主架构文档一致 | | 2025-09-19 | 1.0 | 初始版本,基于现有测试基础设施 | --- **文档状态**: 正式版 **下次评审**: 2025-12-19