# 后端模块包测试规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 2.0 | 2025-12-26 | 基于实际测试实现重写,修正不准确的描述 | James (Claude Code) | | 1.0 | 2025-12-26 | 从测试策略文档拆分,专注后端模块包测试 | James (Claude Code) | ## 概述 本文档定义了后端模块包的测试标准和最佳实践,基于项目实际的测试实现经验总结。 ### 目标包 - **packages/core-module**: 核心模块(用户、认证、文件等) - **allin-packages/**: AllIn业务模块(订单、企业、渠道等) ### 实际测试架构 **重要**: 项目的测试采用**集中式管理**模式: - 测试文件位于各模块包内的 `tests/` 目录 - 使用共享测试工具 `@d8d/shared-test-util` - 集成测试使用真实PostgreSQL数据库 - 单元测试使用mock隔离依赖 ## 测试框架栈 ### 测试运行器 - **Vitest**: 统一的测试运行器 - **配置**: 关闭文件并行测试 (`fileParallelism: false`) 避免数据库连接冲突 ### 测试工具 - **hono/testing**: `testClient` - API路由测试 - **@d8d/shared-test-util**: 共享测试基础设施 - `IntegrationTestDatabase` - 集成测试数据库管理 - `setupIntegrationDatabaseHooksWithEntities` - 测试生命周期钩子 - `TestDataFactory` - 测试数据工厂 - **TypeORM**: 数据库操作和实体管理 ### Mock工具 - **vitest.mock**: 依赖项模拟 - **vi.mocked**: 类型安全的mock操作 ## 测试文件结构 ### 实际项目结构 ``` packages/core-module/ ├── auth-module/ │ └── tests/ │ ├── integration/ │ │ ├── auth.integration.test.ts │ │ ├── phone-decrypt.integration.test.ts │ │ └── system-config-integration.test.ts │ └── unit/ │ └── mini-auth.service.test.ts ├── file-module/ │ └── tests/ │ ├── integration/ │ │ └── file.routes.integration.test.ts │ └── unit/ │ └── file.service.test.ts └── user-module/ └── tests/ ├── integration/ │ ├── role.integration.test.ts │ └── user.routes.integration.test.ts └── utils/ ├── integration-test-db.ts # 模块专用测试工具 └── integration-test-utils.ts allin-packages/order-module/ ├── src/ │ ├── routes/ │ ├── services/ │ ├── entities/ │ └── schemas/ ├── tests/ │ ├── integration/ │ │ ├── order.integration.test.ts │ │ └── talent-employment.integration.test.ts │ └── utils/ │ └── test-data-factory.ts └── vitest.config.ts ``` ### 配置文件模板 ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'dist/', 'tests/', '**/*.d.ts', '**/*.config.*' ] }, // 关键: 关闭并行测试避免数据库连接冲突 fileParallelism: false } }); ``` ## 集成测试最佳实践 ### 1. 使用hono/testing测试API ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { testClient } from 'hono/testing'; import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util'; import { userRoutes } from '../src/routes'; import { UserEntity } from '../src/entities/user.entity'; import { Role } from '../src/entities/role.entity'; import { File } from '@d8d/core-module/file-module'; import { TestDataFactory } from './utils/integration-test-db'; // 设置集成测试钩子 setupIntegrationDatabaseHooksWithEntities([UserEntity, Role, File]) describe('用户路由API集成测试', () => { let client: ReturnType>; let authService: AuthService; let testToken: string; let testUser: any; beforeEach(async () => { // 创建测试客户端 client = testClient(userRoutes); // 获取数据源 const dataSource = await IntegrationTestDatabase.getDataSource(); if (!dataSource) throw new Error('Database not initialized'); // 初始化服务 const userService = new UserService(dataSource); authService = new AuthService(userService); // 创建测试用户并生成token testUser = await TestDataFactory.createTestUser(dataSource, { username: 'testuser_auth', email: 'testuser_auth@example.com' }); testToken = authService.generateToken(testUser); }); describe('用户创建路由测试', () => { it('应该拒绝无认证令牌的用户创建请求', async () => { const userData = { username: 'testuser_create', email: 'testcreate@example.com', password: 'TestPassword123!', nickname: 'Test User', phone: '13800138001' }; const response = await client.index.$post({ json: userData }); // 应该返回401状态码,因为缺少认证 expect(response.status).toBe(401); if (response.status === 401) { const responseData = await response.json(); expect(responseData.message).toContain('Authorization header missing'); } }); it('应该成功创建用户(使用有效认证令牌)', async () => { const userData = { username: 'testuser_create_success', email: 'testcreate_success@example.com', password: 'TestPassword123!', nickname: 'Test User Success' }; const response = await client.index.$post({ json: userData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); const responseData = await response.json(); expect(responseData.data).toHaveProperty('id'); expect(responseData.data.username).toBe('testuser_create_success'); }); }); describe('用户查询路由测试', () => { it('应该支持分页查询用户列表', async () => { // 创建多个测试用户 const dataSource = await IntegrationTestDatabase.getDataSource(); for (let i = 0; i < 15; i++) { await TestDataFactory.createTestUser(dataSource, { username: `pageuser_${i}` }); } const response = await client.index.$get({ query: { page: '1', pageSize: '10' } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); const responseData = await response.json(); expect(responseData.data).toHaveLength(10); expect(responseData.total).toBeGreaterThanOrEqual(15); }); }); }); ``` ### 2. 测试数据工厂模式 ```typescript // tests/utils/integration-test-db.ts import { DataSource } from 'typeorm'; import { UserEntity } from '../../src/entities/user.entity'; import { Role } from '../../src/entities/role.entity'; /** * 测试数据工厂类 */ export class TestDataFactory { /** * 创建测试用户数据 */ static createUserData(overrides: Partial = {}): Partial { const timestamp = Date.now(); return { username: `testuser_${timestamp}`, password: 'TestPassword123!', email: `test_${timestamp}@example.com`, phone: `138${timestamp.toString().slice(-8)}`, nickname: `Test User ${timestamp}`, name: `Test Name ${timestamp}`, isDisabled: 0, isDeleted: 0, ...overrides }; } /** * 创建测试角色数据 */ static createRoleData(overrides: Partial = {}): Partial { const timestamp = Date.now(); return { name: `test_role_${timestamp}`, description: `Test role description ${timestamp}`, ...overrides }; } /** * 在数据库中创建测试用户 */ static async createTestUser( dataSource: DataSource, overrides: Partial = {} ): Promise { const userData = this.createUserData(overrides); const userRepository = dataSource.getRepository(UserEntity); const user = userRepository.create(userData); return await userRepository.save(user); } /** * 在数据库中创建测试角色 */ static async createTestRole( dataSource: DataSource, overrides: Partial = {} ): Promise { const roleData = this.createRoleData(overrides); const roleRepository = dataSource.getRepository(Role); const role = roleRepository.create(roleData); return await roleRepository.save(role); } } ``` ### 3. 集成测试断言工具 ```typescript // tests/utils/integration-test-utils.ts import { IntegrationTestDatabase } from '@d8d/shared-test-util'; import { UserEntity } from '../../src/entities/user.entity'; /** * 集成测试断言工具 */ export class IntegrationTestAssertions { /** * 断言响应状态码 */ static expectStatus(response: { status: number }, expectedStatus: number): void { if (response.status !== expectedStatus) { throw new Error(`Expected status ${expectedStatus}, but got ${response.status}`); } } /** * 断言用户存在于数据库中 */ static async expectUserToExist(username: string): Promise { const dataSource = await IntegrationTestDatabase.getDataSource(); if (!dataSource) { throw new Error('Database not initialized'); } const userRepository = dataSource.getRepository(UserEntity); const user = await userRepository.findOne({ where: { username } }); if (!user) { throw new Error(`Expected user ${username} to exist in database`); } } /** * 断言用户不存在于数据库中 */ static async expectUserNotToExist(username: string): Promise { const dataSource = await IntegrationTestDatabase.getDataSource(); if (!dataSource) { throw new Error('Database not initialized'); } const userRepository = dataSource.getRepository(UserEntity); const user = await userRepository.findOne({ where: { username } }); if (user) { throw new Error(`Expected user ${username} not to exist in database`); } } } ``` ### 4. 测试完整业务流程 ```typescript describe('订单管理完整流程测试', () => { let client: ReturnType>; let testToken: string; let testPlatform: any; let testCompany: any; let testChannel: any; beforeEach(async () => { client = testClient(orderRoutes); const dataSource = await IntegrationTestDatabase.getDataSource(); // 创建测试基础数据 testPlatform = await createTestPlatform(dataSource); testCompany = await createTestCompany(dataSource); testChannel = await createTestChannel(dataSource); // 创建测试用户并生成token const testUser = await TestDataFactory.createTestUser(dataSource); testToken = JWTUtil.generateToken({ userId: testUser.id, username: testUser.username }); }); it('应该完成订单创建到分配人员的完整流程', async () => { // 1. 创建订单 const createResponse = await client.create.$post({ json: { orderName: '测试订单', platformId: testPlatform.id, companyId: testCompany.id, channelId: testChannel.id, expectedStartDate: new Date().toISOString(), orderStatus: 'DRAFT' } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(createResponse.status).toBe(200); const { data: order } = await createResponse.json(); // 2. 更新订单状态 const updateResponse = await client[':id'].$patch({ param: { id: order.id }, json: { orderStatus: 'ACTIVE' } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(updateResponse.status).toBe(200); // 3. 分配人员到订单 const assignResponse = await client.assign.$post({ json: { orderId: order.id, personIds: [1, 2, 3] } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(assignResponse.status).toBe(200); // 4. 验证订单详情 const detailResponse = await client[':id'].$get({ param: { id: order.id } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(detailResponse.status).toBe(200); const { data: orderDetail } = await detailResponse.json(); expect(orderDetail.orderStatus).toBe('ACTIVE'); }); }); ``` ## 单元测试最佳实践 ### 1. Service层单元测试 ```typescript import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { DataSource } from 'typeorm'; import { FileService } from '../../src/services/file.service'; import { File } from '../../src/entities/file.entity'; import { MinioService } from '../../src/services/minio.service'; // Mock依赖项 vi.mock('../../src/services/minio.service'); vi.mock('@d8d/shared-utils', () => ({ logger: { error: vi.fn(), db: vi.fn() }, ErrorSchema: {} })); vi.mock('uuid', () => ({ v4: () => 'test-uuid-123' })); describe('FileService', () => { let mockDataSource: DataSource; let fileService: FileService; beforeEach(() => { mockDataSource = { getRepository: vi.fn(() => ({ findOne: vi.fn(), findOneBy: vi.fn(), save: vi.fn(), create: vi.fn() })) } as unknown as DataSource; fileService = new FileService(mockDataSource); }); afterEach(() => { vi.clearAllMocks(); }); describe('createFile', () => { it('应该成功创建文件并生成上传策略', async () => { const mockFileData = { name: 'test.txt', type: 'text/plain', size: 1024, uploadUserId: 1 }; const mockUploadPolicy = { 'x-amz-algorithm': 'test-algorithm', 'x-amz-credential': 'test-credential', host: 'https://minio.example.com' }; const mockSavedFile = { id: 1, ...mockFileData, path: '1/test-uuid-123-test.txt', uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() }; const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy); vi.mocked(MinioService).mockImplementation(() => ({ generateUploadPolicy: mockGenerateUploadPolicy } as unknown as MinioService)); // Mock GenericCrudService的create方法 vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File); const result = await fileService.createFile(mockFileData); expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt'); expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({ name: 'test.txt', path: '1/test-uuid-123-test.txt', uploadUserId: 1 })); expect(result).toEqual({ file: mockSavedFile, uploadPolicy: mockUploadPolicy }); }); it('应该处理文件创建错误', async () => { const mockFileData = { name: 'test.txt', uploadUserId: 1 }; const mockGenerateUploadPolicy = vi.fn() .mockRejectedValue(new Error('MinIO error')); vi.mocked(MinioService).mockImplementation(() => ({ generateUploadPolicy: mockGenerateUploadPolicy } as unknown as MinioService)); await expect(fileService.createFile(mockFileData)) .rejects.toThrow('MinIO error'); }); }); }); ``` ### 2. Schema验证测试 ```typescript import { describe, it, expect } from 'vitest'; import { CreatePlatformSchema, UpdatePlatformSchema } from '../../src/schemas/platform.schema'; describe('平台Schema验证测试', () => { describe('CreatePlatformSchema', () => { it('应该验证有效的平台数据', () => { const data = { platformName: '测试平台', contactEmail: 'test@example.com', contactPhone: '13800138000' }; const result = CreatePlatformSchema.safeParse(data); expect(result.success).toBe(true); if (result.success) { expect(result.data.platformName).toBe('测试平台'); expect(result.data.contactEmail).toBe('test@example.com'); } }); it('应该验证空字符串转换为undefined', () => { const data = { platformName: '测试平台', contactEmail: '' // 空字符串应该转换为undefined }; const result = CreatePlatformSchema.safeParse(data); expect(result.success).toBe(true); if (result.success) { expect(result.data.contactEmail).toBeUndefined(); } }); it('应该拒绝无效的邮箱格式', () => { const data = { platformName: '测试平台', contactEmail: 'invalid-email' }; const result = CreatePlatformSchema.safeParse(data); expect(result.success).toBe(false); }); it('应该拒绝缺失必填字段', () => { const data = { contactEmail: 'test@example.com' // 缺少 platformName }; const result = CreatePlatformSchema.safeParse(data); expect(result.success).toBe(false); }); }); }); ``` ### 3. 工具函数测试 ```typescript import { describe, it, expect } from 'vitest'; import { generateOrderNumber, calculateOrderAmount } from '../../src/utils/order.util'; describe('订单工具函数测试', () => { describe('generateOrderNumber', () => { it('应该生成唯一订单号', () => { const orderNumber1 = generateOrderNumber(); const orderNumber2 = generateOrderNumber(); expect(orderNumber1).not.toBe(orderNumber2); expect(orderNumber1).toMatch(/^ORD\d{13}$/); // ORD + 13位时间戳 }); it('应该生成带前缀的订单号', () => { const orderNumber = generateOrderNumber('TEST'); expect(orderNumber).toMatch(/^TEST\d{13}$/); }); }); describe('calculateOrderAmount', () => { it('应该正确计算订单金额', () => { const items = [ { quantity: 2, unitPrice: 100 }, { quantity: 3, unitPrice: 50 } ]; const total = calculateOrderAmount(items); expect(total).toBe(350); // 2*100 + 3*50 }); it('应该处理空数组', () => { const total = calculateOrderAmount([]); expect(total).toBe(0); }); it('应该处理小数精度', () => { const items = [ { quantity: 1, unitPrice: 99.99 }, { quantity: 2, unitPrice: 50.005 } ]; const total = calculateOrderAmount(items); expect(total).toBeCloseTo(200, 2); }); }); }); ``` ## 共享测试工具使用 ### IntegrationTestDatabase 来自 `@d8d/shared-test-util`,用于管理集成测试数据库: ```typescript import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util'; import { UserEntity } from '../src/entities/user.entity'; import { Role } from '../src/entities/role.entity'; // 设置测试生命周期钩子 setupIntegrationDatabaseHooksWithEntities([UserEntity, Role]) describe('集成测试', () => { it('使用集成测试数据库', async () => { // 获取数据源 const dataSource = await IntegrationTestDatabase.getDataSource(); // 使用Repository const userRepo = dataSource.getRepository(UserEntity); const user = await userRepo.findOne({ where: { id: 1 } }); expect(user).toBeDefined(); }); }); ``` ### 自定义模块测试工具 每个模块可以定义自己的测试工具: ```typescript // tests/utils/integration-test-db.ts import { DataSource } from 'typeorm'; import { IntegrationTestDatabase } from '@d8d/shared-test-util'; import { Order } from '../../src/entities/order.entity'; import { Talent } from '../../src/entities/talent.entity'; export class OrderTestDataFactory { /** * 创建测试订单数据 */ static async createTestOrder( dataSource: DataSource, overrides: Partial = {} ): Promise { const timestamp = Date.now(); const orderData = { orderName: `测试订单_${timestamp}`, orderStatus: 'DRAFT', workStatus: 'NOT_WORKING', ...overrides }; const orderRepo = dataSource.getRepository(Order); const order = orderRepo.create(orderData); return await orderRepo.save(order); } /** * 创建测试残疾人数据 */ static async createTestTalent( dataSource: DataSource, overrides: Partial = {} ): Promise { const timestamp = Date.now(); const talentData = { name: `测试残疾人_${timestamp}`, idCard: `110101199001011${timestamp.toString().slice(-3)}`, phone: `138${timestamp.toString().slice(-8)}`, ...overrides }; const talentRepo = dataSource.getRepository(Talent); const talent = talentRepo.create(talentData); return await talentRepo.save(talent); } } ``` ## 测试命名约定 ### 文件命名 - 集成测试:`[feature].integration.test.ts` - 单元测试:`[component].test.ts` - 测试工具:`integration-test-db.ts`、`integration-test-utils.ts` ### 测试描述 ```typescript describe('[模块名]', () => { describe('[功能名]', () => { it('应该[预期行为]', async () => { }); it('应该拒绝[错误情况]', async () => { }); }); }); ``` ## 运行测试 ### 在模块包中运行 ```bash # 进入模块目录 cd packages/core-module/user-module # 或 cd allin-packages/order-module # 运行所有测试 pnpm test # 运行集成测试 pnpm test:integration # 运行单元测试 pnpm test:unit # 生成覆盖率报告 pnpm test:coverage # 运行特定测试 pnpm test --testNamePattern="应该成功创建用户" # 监听模式 pnpm test --watch ``` ### 根目录运行 ```bash # 运行所有模块测试 pnpm test # 运行特定目录的测试 pnpm test "packages/core-module/**/*.test.ts" pnpm test "allin-packages/**/*.test.ts" ``` ## 覆盖率标准 | 测试类型 | 最低要求 | 目标要求 | 关键模块要求 | |----------|----------|----------|--------------| | 集成测试 | 60% | 70% | 80% | | 单元测试 | 50% | 60% | 70% | **关键模块定义**: - 认证授权Service:70%单元测试覆盖率 - 数据库操作Service:70%单元测试覆盖率 - API路由:80%集成测试覆盖率 - Schema验证:90%单元测试覆盖率 ## 常见错误避免 ### ❌ 不要在集成测试中mock数据库 ```typescript // 错误:集成测试中mock数据库Repository vi.mock('typeorm', () => ({ getRepository: vi.fn(() => mockRepo) })); // 正确:使用真实的数据库和Repository const dataSource = await IntegrationTestDatabase.getDataSource(); const userRepo = dataSource.getRepository(UserEntity); ``` ### ❌ 不要硬编码测试数据 ```typescript // 错误:硬编码用户名 it('应该创建用户', async () => { await createTestUser(dataSource, { username: 'testuser' }); }); // 正确:使用时间戳生成唯一数据 it('应该创建用户', async () => { await TestDataFactory.createTestUser(dataSource); // 自动生成唯一用户名 }); ``` ### ❌ 不要忽略认证测试 ```typescript // 错误:不测试认证 it('应该创建用户', async () => { const response = await client.create.$post({ json: userData }); expect(response.status).toBe(200); }); // 正确:测试有认证和无认证两种情况 it('应该拒绝无认证的请求', async () => { const response = await client.create.$post({ json: userData }); expect(response.status).toBe(401); }); it('应该接受有效认证的请求', async () => { const response = await client.create.$post({ json: userData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); }); ``` ### ❌ 不要忘记清理测试数据 ```typescript // 错误:不使用setupIntegrationDatabaseHooksWithEntities describe('测试', () => { beforeEach(async () => { await IntegrationTestDatabase.initializeWithEntities([UserEntity]); }); // 没有afterEach清理 }); // 正确:使用setupIntegrationDatabaseHooksWithEntities自动管理 setupIntegrationDatabaseHooksWithEntities([UserEntity]) describe('测试', () => { // beforeEach和afterEach自动设置 }); ``` ## 调试技巧 ### 1. 运行特定测试 ```bash # 运行特定测试文件 pnpm test user.routes.integration.test.ts # 运行匹配名称的测试 pnpm test --testNamePattern="应该成功创建用户" # 显示详细输出 pnpm test --reporter=verbose ``` ### 2. 调试SQL查询 ```typescript // 在测试中启用SQL日志 const dataSource = await IntegrationTestDatabase.getDataSource(); // 查看实际执行的SQL dataSource.driver.createQueryRunner('debug').query('SELECT * FROM users'); ``` ### 3. 使用only专注某个测试 ```typescript describe.only('专注这个测试套件', () => { it.only('只运行这个测试', async () => { // ... }); }); ``` ## 参考实现 ### 核心模块 - 用户模块:`packages/core-module/user-module/tests/` - 认证模块:`packages/core-module/auth-module/tests/` - 文件模块:`packages/core-module/file-module/tests/` ### AllIn业务模块 - 订单模块:`allin-packages/order-module/tests/` - 企业模块:`allin-packages/company-module/tests/` - 渠道模块:`allin-packages/channel-module/tests/` - 残疾人模块:`allin-packages/disability-module/tests/` ### 共享测试工具 - 测试基础设施:`packages/shared-test-util/src/` ## 相关文档 - [测试策略概述](./testing-strategy.md) - [Web UI包测试规范](./web-ui-testing-standards.md) - [Web Server包测试规范](./web-server-testing-standards.md) - [Mini UI包测试规范](./mini-ui-testing-standards.md) - [后端模块包开发规范](./backend-module-package-standards.md) --- **文档状态**: 正式版 **适用范围**: packages/core-module、allin-packages/ **基于实际测试实现**: 2025-12-26