# 后端模块包测试规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 1.0 | 2025-12-26 | 从测试策略文档拆分,专注后端模块包测试 | James (Claude Code) | ## 概述 本文档定义了后端模块包的测试标准和最佳实践。 - **目标**: `packages/*-module` - 业务模块包(user-module、auth-module、file-module等) - **测试类型**: 单元测试 + 集成测试 ## 测试框架栈 - **Vitest**: 测试运行器 - **hono/testing**: Hono路由测试 - **TypeORM**: 数据库测试 - **PostgreSQL**: 测试数据库 - **shared-test-util**: 共享测试基础设施 ## 测试分层策略 ### 1. 单元测试(Unit Tests) - **范围**: 单个服务、工具函数、Schema验证 - **目标**: 验证独立单元的正确性 - **位置**: `packages/*-module/tests/unit/**/*.test.ts` - **框架**: Vitest - **覆盖率目标**: ≥ 80% ### 2. 集成测试(Integration Tests) - **范围**: 路由、服务、数据库集成 - **目标**: 验证模块内部组件协作 - **位置**: `packages/*-module/tests/integration/**/*.test.ts` - **框架**: Vitest + hono/testing + TypeORM - **覆盖率目标**: ≥ 60% ## 测试文件结构 ``` packages/user-module/ ├── src/ │ ├── entities/ │ │ ├── user.entity.ts │ │ └── role.entity.ts │ ├── services/ │ │ ├── user.service.ts │ │ └── role.service.ts │ ├── schemas/ │ │ ├── user.schema.ts │ │ └── role.schema.ts │ └── routes/ │ ├── user.routes.ts │ └── role.routes.ts └── tests/ ├── unit/ │ ├── services/ │ │ ├── user.service.test.ts │ │ └── role.service.test.ts │ └── schemas/ │ ├── user.schema.test.ts │ └── role.schema.test.ts ├── integration/ │ ├── routes/ │ │ ├── user.routes.integration.test.ts │ │ └── role.routes.integration.test.ts │ └── services/ │ └── user.service.integration.test.ts └── fixtures/ ├── test-db.ts # 测试数据库配置 └── factories.ts # 测试数据工厂 ``` ## 单元测试最佳实践 ### 1. 测试Service层 ```typescript import { describe, it, expect, vi, beforeEach } from 'vitest'; import { UserService } from '../src/services/user.service'; import { User } from '../src/entities/user.entity'; import { Repository } from 'typeorm'; describe('UserService', () => { let userService: UserService; let mockUserRepo: Partial>; beforeEach(() => { // 模拟Repository mockUserRepo = { findOne: vi.fn(), find: vi.fn(), create: vi.fn(), save: vi.fn(), update: vi.fn(), delete: vi.fn() }; userService = new UserService(mockUserRepo as Repository); }); describe('findById()', () => { it('应该返回用户数据', async () => { // Arrange const mockUser = { id: 1, username: 'testuser', email: 'test@example.com' }; mockUserRepo.findOne?.mockResolvedValue(mockUser); // Act const result = await userService.findById(1); // Assert expect(result).toEqual(mockUser); expect(mockUserRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } }); }); it('应该返回null当用户不存在', async () => { mockUserRepo.findOne?.mockResolvedValue(null); const result = await userService.findById(999); expect(result).toBeNull(); }); }); describe('createUser()', () => { it('应该创建新用户', async () => { const userData = { username: 'newuser', email: 'new@example.com', password: 'hashedpassword' }; const createdUser = { id: 1, ...userData }; mockUserRepo.create?.mockReturnValue(createdUser); mockUserRepo.save?.mockResolvedValue(createdUser); const result = await userService.createUser(userData); expect(result).toEqual(createdUser); expect(mockUserRepo.create).toHaveBeenCalledWith(userData); expect(mockUserRepo.save).toHaveBeenCalledWith(createdUser); }); it('应该抛出错误当用户名已存在', async () => { mockUserRepo.findOne?.mockResolvedValue({ id: 1, username: 'existing' }); await expect( userService.createUser({ username: 'existing', email: 'test@example.com', password: 'hash' }) ).rejects.toThrow('用户名已存在'); }); }); }); ``` ### 2. 测试Schema验证 ```typescript import { describe, it, expect } from 'vitest'; import { userSchema } from '../src/schemas/user.schema'; describe('userSchema', () => { it('应该验证有效的用户数据', async () => { const validData = { username: 'testuser', email: 'test@example.com', password: 'password123' }; const result = await userSchema.parseAsync(validData); expect(result).toEqual(validData); }); it('应该拒绝无效的邮箱格式', async () => { const invalidData = { username: 'testuser', email: 'invalid-email', password: 'password123' }; await expect(userSchema.parseAsync(invalidData)).rejects.toThrow(); }); it('应该拒绝空密码', async () => { const invalidData = { username: 'testuser', email: 'test@example.com', password: '' }; await expect(userSchema.parseAsync(invalidData)).rejects.toThrow(); }); }); ``` ### 3. 测试工具函数 ```typescript import { describe, it, expect } from 'vitest'; import { hashPassword, comparePassword } from '../src/utils/password.util'; describe('密码工具函数', () => { describe('hashPassword()', () => { it('应该返回哈希后的密码', async () => { const password = 'password123'; const hash = await hashPassword(password); expect(hash).not.toBe(password); expect(hash).toMatch(/^\$2[ayb]\$.{56}$/); // bcrypt格式 }); it('应该为相同密码生成不同的哈希值', async () => { const password = 'password123'; const hash1 = await hashPassword(password); const hash2 = await hashPassword(password); expect(hash1).not.toBe(hash2); }); }); describe('comparePassword()', () => { it('应该返回true当密码匹配', async () => { const password = 'password123'; const hash = await hashPassword(password); const result = await comparePassword(password, hash); expect(result).toBe(true); }); it('应该返回false当密码不匹配', async () => { const hash = await hashPassword('password123'); const result = await comparePassword('wrongpassword', hash); expect(result).toBe(false); }); }); }); ``` ## 集成测试最佳实践 ### 1. 测试路由层 ```typescript import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { integrateRoutes } from 'hono/testing'; import { Hono } from 'hono'; import { getTestDataSource } from './fixtures/test-db'; import { userRoutes } from '../src/routes/user.routes'; import { seedTestUser } from './fixtures/factories'; describe('用户路由集成测试', () => { let dataSource: DataSource; let app: Hono; beforeEach(async () => { dataSource = await getTestDataSource(); await dataSource.initialize(); app = new Hono(); app.route('/api/users', userRoutes); }); afterEach(async () => { await dataSource.destroy(); }); describe('GET /api/users', () => { it('应该返回用户列表', async () => { // Arrange await seedTestUser(dataSource, { username: 'user1' }); await seedTestUser(dataSource, { username: 'user2' }); // Act const res = await integrateRoutes(app).GET('/api/users'); // Assert expect(res.status).toBe(200); const json = await res.json(); expect(json.data).toHaveLength(2); expect(json.data[0].username).toBe('user1'); }); it('应该支持分页查询', async () => { // 创建15个用户 for (let i = 1; i <= 15; i++) { await seedTestUser(dataSource, { username: `user${i}` }); } // 查询第一页 const res1 = await integrateRoutes(app).GET('/api/users?page=1&pageSize=10'); expect(res1.status).toBe(200); const json1 = await res1.json(); expect(json1.data).toHaveLength(10); expect(json1.total).toBe(15); // 查询第二页 const res2 = await integrateRoutes(app).GET('/api/users?page=2&pageSize=10'); expect(res2.status).toBe(200); const json2 = await res2.json(); expect(json2.data).toHaveLength(5); }); }); describe('GET /api/users/:id', () => { it('应该返回用户详情', async () => { const user = await seedTestUser(dataSource, { username: 'testuser' }); const res = await integrateRoutes(app).GET(`/api/users/${user.id}`); expect(res.status).toBe(200); const json = await res.json(); expect(json.username).toBe('testuser'); }); it('应该返回404当用户不存在', async () => { const res = await integrateRoutes(app).GET('/api/users/99999'); expect(res.status).toBe(404); }); }); describe('POST /api/users', () => { it('应该创建新用户', async () => { const userData = { username: 'newuser', email: 'new@example.com', password: 'password123' }; const res = await integrateRoutes(app).POST('/api/users', { json: userData }); expect(res.status).toBe(201); const json = await res.json(); expect(json.username).toBe('newuser'); // 验证数据库中的数据 const userRepo = dataSource.getRepository(User); const user = await userRepo.findOne({ where: { username: 'newuser' } }); expect(user).toBeDefined(); }); it('应该拒绝重复的用户名', async () => { await seedTestUser(dataSource, { username: 'existing' }); const res = await integrateRoutes(app).POST('/api/users', { json: { username: 'existing', email: 'different@example.com', password: 'password123' } }); expect(res.status).toBe(400); const json = await res.json(); expect(json.error).toContain('用户名已存在'); }); }); }); ``` ### 2. 测试Service与数据库集成 ```typescript describe('UserService与数据库集成', () => { let dataSource: DataSource; let userService: UserService; beforeEach(async () => { dataSource = await getTestDataSource(); await dataSource.initialize(); const userRepo = dataSource.getRepository(User); userService = new UserService(userRepo); }); afterEach(async () => { await dataSource.destroy(); }); it('应该从数据库查询用户', async () => { // 创建测试数据 const userRepo = dataSource.getRepository(User); const createdUser = await userRepo.save({ username: 'testuser', email: 'test@example.com', password: 'hashedpassword' }); // 测试查询 const foundUser = await userService.findById(createdUser.id); expect(foundUser).toBeDefined(); expect(foundUser?.username).toBe('testuser'); }); it('应该正确处理事务', async () => { await dataSource.transaction(async (manager) => { const user1 = await manager.save(User, { username: 'user1', email: 'user1@example.com', password: 'hash' }); const user2 = await manager.save(User, { username: 'user2', email: 'user2@example.com', password: 'hash' }); expect(user1.id).toBeDefined(); expect(user2.id).toBeDefined(); }); }); }); ``` ### 3. 测试中间件集成 ```typescript import { authMiddleware } from '../src/middleware/auth.middleware'; import { generateToken } from '@d8d/shared-utils/jwt.util'; describe('认证中间件集成测试', () => { it('应该允许有效token的请求', async () => { const app = new Hono(); app.use('/api/protected/*', authMiddleware); app.get('/api/protected/data', (c) => c.json({ message: 'Protected data' })); const token = generateToken({ userId: 1, username: 'testuser' }); const res = await integrateRoutes(app).GET('/api/protected/data', { headers: { Authorization: `Bearer ${token}` } }); expect(res.status).toBe(200); expect(await res.json()).toEqual({ message: 'Protected data' }); }); it('应该拒绝无效token的请求', async () => { const app = new Hono(); app.use('/api/protected/*', authMiddleware); app.get('/api/protected/data', (c) => c.json({ message: 'Protected data' })); const res = await integrateRoutes(app).GET('/api/protected/data', { headers: { Authorization: 'Bearer invalid-token' } }); expect(res.status).toBe(401); }); }); ``` ## 测试数据工厂模式 ### factories.ts ```typescript import { User } from '../src/entities/user.entity'; import { Repository } from 'typeorm'; export function buildUser(overrides = {}): Partial { return { id: Math.floor(Math.random() * 10000), username: `testuser_${Date.now()}`, email: `test_${Date.now()}@example.com`, password: 'hashedpassword', role: 'user', active: true, createdAt: new Date(), updatedAt: new Date(), ...overrides }; } export async function createTestUser( userRepo: Repository, overrides = {} ): Promise { const userData = buildUser(overrides); const user = userRepo.create(userData); return await userRepo.save(user); } // 批量创建测试数据 export async function createTestUsers( userRepo: Repository, count: number, overrides = {} ): Promise { const users = []; for (let i = 0; i < count; i++) { const user = await createTestUser(userRepo, { username: `user${i}`, email: `user${i}@example.com`, ...overrides }); users.push(user); } return users; } ``` ## 数据库测试策略 ### 1. 使用事务回滚 ```typescript describe('使用事务回滚', () => { let queryRunner: QueryRunner; let dataSource: DataSource; beforeEach(async () => { dataSource = await getTestDataSource(); await dataSource.initialize(); queryRunner = dataSource.createQueryRunner(); await queryRunner.startTransaction(); }); afterEach(async () => { await queryRunner.rollbackTransaction(); await queryRunner.release(); await dataSource.destroy(); }); it('每个测试后自动回滚', async () => { const userRepo = queryRunner.manager.getRepository(User); await userRepo.save({ username: 'testuser', email: 'test@example.com', password: 'hash' }); // 数据存在于事务中 const users = await userRepo.find(); expect(users).toHaveLength(1); // 测试结束后自动回滚,数据库为空 }); }); ``` ### 2. 使用测试数据库 ```typescript // tests/fixtures/test-db.ts import { DataSource } from 'typeorm'; import { User } from '../src/entities/user.entity'; import { Role } from '../src/entities/role.entity'; export async function getTestDataSource(): Promise { return new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'test_password', database: 'test_d8dai', // 独立的测试数据库 entities: [User, Role], synchronize: true, // 自动同步表结构 dropSchema: true, // 每次测试前清空数据库 logging: false // 测试环境关闭SQL日志 }); } ``` ## 覆盖率标准 | 测试类型 | 最低要求 | 目标要求 | 关键模块要求 | |----------|----------|----------|--------------| | 单元测试 | 70% | 80% | 90% | | 集成测试 | 50% | 60% | 70% | **关键模块定义**: - 认证授权Service:90%单元测试覆盖率 - 数据库操作Service:85%单元测试覆盖率 - 核心业务逻辑:80%集成测试覆盖率 ## 运行测试 ### 本地开发 ```bash # 运行所有测试 cd packages/user-module && pnpm test # 只运行单元测试 pnpm test:unit # 只运行集成测试 pnpm test:integration # 生成覆盖率报告 pnpm test:coverage # 运行特定测试文件 pnpm test user.service.test.ts # 运行特定测试用例 pnpm test --testNamePattern="应该创建用户" ``` ### CI/CD ```yaml user-module-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 install - run: cd packages/user-module && pnpm test all-modules-tests: runs-on: ubuntu-latest steps: - run: cd packages/user-module && pnpm test - run: cd packages/auth-module && pnpm test - run: cd packages/file-module && pnpm test ``` ## 测试命名约定 ### 文件命名 - Service单元测试:`[service].service.test.ts` - Schema单元测试:`[schema].schema.test.ts` - 路由集成测试:`[route].routes.integration.test.ts` - Service集成测试:`[service].service.integration.test.ts` ### 测试描述 ```typescript describe('UserService', () => { describe('findById()', () => { it('应该返回用户数据', async () => { }); it('应该返回null当用户不存在', async () => { }); }); describe('createUser()', () => { it('应该创建新用户', async () => { }); it('应该拒绝重复的用户名', async () => { }); }); }); ``` ## 常见错误避免 ### ❌ 不要在单元测试中使用真实数据库 ```typescript // 错误:单元测试连接真实数据库 it('应该查询用户', async () => { const dataSource = await getTestDataSource(); // 不要在单元测试中 // ... }); // 正确:使用模拟的Repository it('应该查询用户', async () => { const mockRepo = { findOne: vi.fn() }; const userService = new UserService(mockRepo); // ... }); ``` ### ❌ 不要在集成测试中mock数据库 ```typescript // 错误:集成测试中mock数据库 vi.mock('typeorm', () => ({ getRepository: vi.fn(() => mockRepo) })); // 正确:使用真实数据库 const dataSource = await getTestDataSource(); const userRepo = dataSource.getRepository(User); ``` ### ❌ 不要共享测试数据 ```typescript // 错误:测试间共享数据 let testUserId: number; it('应该创建用户', async () => { const user = await createUser({ username: 'test' }); testUserId = user.id; // 污染下一个测试 }); it('应该更新用户', async () => { await updateUser(testUserId, { username: 'updated' }); // 依赖上一个测试 }); // 正确:每个测试独立创建数据 it('应该更新用户', async () => { const user = await createTestUser(dataSource); await updateUser(user.id, { username: 'updated' }); }); ``` ## 调试技巧 ### 1. 查看SQL查询 ```typescript const dataSource = new DataSource({ // ... logging: true, // 显示所有SQL maxQueryExecutionTime: 100 // 记录慢查询 }); ``` ### 2. 打印测试数据 ```typescript it('调试测试', async () => { const users = await userService.findAll(); console.debug('用户列表:', users); console.debug('用户数量:', users.length); }); ``` ### 3. 使用only运行特定测试 ```typescript it.only('只运行这个测试', async () => { // ... }); ``` ## 参考实现 - 用户模块包:`packages/user-module/tests/` - 认证模块包:`packages/auth-module/tests/` - 文件模块包:`packages/file-module/tests/` ## 相关文档 - [测试策略概述](./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/*-module