# Web Server 包测试规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 1.0 | 2025-12-26 | 从测试策略文档拆分,专注Web Server包测试 | James (Claude Code) | ## 概述 本文档定义了Web Server包的测试标准和最佳实践。 - **目标**: `packages/server` - API服务器包 - **测试类型**: 集成测试(模块间协作测试) ## 测试框架栈 - **Vitest**: 测试运行器 - **hono/testing**: Hono官方测试工具 - **TypeORM**: 数据库测试 - **PostgreSQL**: 测试数据库 - **shared-test-util**: 共享测试基础设施 ## 测试策略 ### 集成测试(Integration Tests) - **范围**: 模块间集成、API端点集成 - **目标**: 验证各业务模块在server环境中的正确集成 - **位置**: `packages/server/tests/integration/**/*.test.ts` - **框架**: Vitest + hono/testing + TypeORM - **覆盖率目标**: ≥ 60% ## 测试文件结构 ``` packages/server/ ├── src/ │ ├── api.ts # API路由导出 │ └── index.ts # 服务器入口 └── tests/ ├── integration/ │ ├── auth.integration.test.ts # 认证集成测试 │ ├── users.integration.test.ts # 用户管理集成测试 │ ├── files.integration.test.ts # 文件管理集成测试 │ └── api-integration.test.ts # API端点集成测试 └── fixtures/ ├── test-db.ts # 测试数据库配置 └── test-data.ts # 测试数据工厂 ``` ## 集成测试最佳实践 ### 1. 使用hono/testing测试API端点 ```typescript import { test } from 'vitest'; import { integrateRoutes } from 'hono/testing'; import app from '../src/api'; describe('POST /api/auth/login', () => { it('应该成功登录并返回token', async () => { // Arrange const testData = { username: 'testuser', password: 'password123' }; // Act const res = await integrateRoutes(app).POST('/api/auth/login', { json: testData }); // Assert expect(res.status).toBe(200); const json = await res.json(); expect(json).toHaveProperty('token'); expect(json.user.username).toBe('testuser'); }); }); ``` ### 2. 测试数据库集成 ```typescript import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { App } from 'hono'; import { DataSource } from 'typeorm'; import { getTestDataSource } from './fixtures/test-db'; import { User } from '@d8d/user-module/entities'; describe('用户管理集成测试', () => { let dataSource: DataSource; beforeEach(async () => { // 设置测试数据库 dataSource = await getTestDataSource(); await dataSource.initialize(); }); afterEach(async () => { // 清理测试数据库 await dataSource.destroy(); }); it('应该创建用户并返回用户数据', async () => { const app = new App(); app.route('/api/users', userRoutes); const res = await integrateRoutes(app).POST('/api/users', { json: { username: 'testuser', email: 'test@example.com', password: 'password123' } }); expect(res.status).toBe(201); // 验证数据库中的数据 const userRepo = dataSource.getRepository(User); const user = await userRepo.findOne({ where: { username: 'testuser' } }); expect(user).toBeDefined(); expect(user?.email).toBe('test@example.com'); }); }); ``` ### 3. 测试认证中间件 ```typescript import { generateToken } from '@d8d/shared-utils/jwt.util'; describe('认证保护的API端点', () => { it('应该拒绝未认证的请求', async () => { const res = await integrateRoutes(app).GET('/api/users/me'); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: '未授权访问' }); }); it('应该接受有效token的请求', async () => { const token = generateToken({ userId: 1, username: 'testuser' }); const res = await integrateRoutes(app).GET('/api/users/me', { headers: { Authorization: `Bearer ${token}` } }); expect(res.status).toBe(200); const json = await res.json(); expect(json.username).toBe('testuser'); }); }); ``` ### 4. 测试模块间集成 ```typescript describe('认证与用户模块集成', () => { it('应该成功注册用户并自动登录', async () => { const app = new App(); app.route('/api/auth', authRoutes); app.route('/api/users', userRoutes); // 1. 注册用户 const registerRes = await integrateRoutes(app).POST('/api/auth/register', { json: { username: 'newuser', email: 'new@example.com', password: 'password123' } }); expect(registerRes.status).toBe(201); // 2. 登录 const loginRes = await integrateRoutes(app).POST('/api/auth/login', { json: { username: 'newuser', password: 'password123' } }); expect(loginRes.status).toBe(200); const { token } = await loginRes.json(); expect(token).toBeDefined(); // 3. 使用token访问受保护的端点 const meRes = await integrateRoutes(app).GET('/api/users/me', { headers: { Authorization: `Bearer ${token}` } }); expect(meRes.status).toBe(200); const user = await meRes.json(); expect(user.username).toBe('newuser'); }); }); ``` ### 5. 测试文件上传集成 ```typescript import { createReadStream } from 'fs'; import { FormData } from 'hono/client'; describe('文件上传集成测试', () => { it('应该成功上传文件并返回文件URL', async () => { const formData = new FormData(); formData.append('file', createReadStream('test/fixtures/test-file.png')); const res = await integrateRoutes(app).POST('/api/files/upload', { body: formData }); expect(res.status).toBe(201); const json = await res.json(); expect(json).toHaveProperty('url'); expect(json).toHaveProperty('id'); }); }); ``` ### 6. 测试错误处理 ```typescript describe('API错误处理', () => { it('应该返回404当资源不存在', async () => { const res = await integrateRoutes(app).GET('/api/users/99999'); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: '用户不存在' }); }); it('应该返回400当请求数据无效', async () => { const res = await integrateRoutes(app).POST('/api/users', { json: { username: '', // 无效的用户名 email: 'invalid-email' // 无效的邮箱 } }); expect(res.status).toBe(400); const json = await res.json(); expect(json).toHaveProperty('errors'); }); }); ``` ### 7. 使用共享测试工具 ```typescript import { createIntegrationTestApp, setupTestDatabase, teardownTestDatabase } from '@d8d/shared-test-util'; describe('使用共享测试工具', () => { let dataSource: DataSource; let app: Hono; beforeAll(async () => { dataSource = await setupTestDatabase(); app = await createIntegrationTestApp(dataSource); }); afterAll(async () => { await teardownTestDatabase(dataSource); }); it('应该使用共享工具运行集成测试', async () => { const res = await integrateRoutes(app).GET('/api/users'); expect(res.status).toBe(200); }); }); ``` ## 测试数据管理 ### 测试数据工厂 ```typescript // tests/fixtures/test-data.ts import { User } from '@d8d/user-module/entities'; export function createTestUser(overrides = {}): Partial { return { id: 1, username: 'testuser', email: 'test@example.com', password: 'hashedpassword', role: 'user', active: true, createdAt: new Date(), ...overrides }; } export async function seedTestUser(dataSource: DataSource, userData = {}) { const userRepo = dataSource.getRepository(User); const user = userRepo.create(createTestUser(userData)); return await userRepo.save(user); } ``` ### 数据库清理策略 ```typescript // 选项1: 事务回滚(推荐) describe('使用事务回滚', () => { let queryRunner: QueryRunner; beforeEach(async () => { queryRunner = dataSource.createQueryRunner(); await queryRunner.startTransaction(); }); afterEach(async () => { await queryRunner.rollbackTransaction(); await queryRunner.release(); }); }); // 选项2: 每个测试后清理 describe('使用数据库清理', () => { afterEach(async () => { const entities = dataSource.entityMetadatas; for (const entity of entities) { const repository = dataSource.getRepository(entity.name); await repository.clear(); } }); }); ``` ## 测试命名约定 ### 文件命名 - 集成测试:`[module].integration.test.ts` - API测试:`[endpoint].integration.test.ts` ### 测试描述 ```typescript describe('用户管理API', () => { describe('GET /api/users', () => { it('应该返回用户列表', async () => { }); it('应该支持分页', async () => { }); it('应该支持搜索过滤', async () => { }); }); describe('POST /api/users', () => { it('应该创建新用户', async () => { }); it('应该验证重复用户名', async () => { }); it('应该验证邮箱格式', async () => { }); }); }); ``` ## 环境配置 ### 测试环境变量 ```typescript // vitest.config.ts export default defineConfig({ test: { setupFiles: ['./tests/setup.ts'], env: { NODE_ENV: 'test', DATABASE_URL: 'postgresql://postgres:test_password@localhost:5432/test_d8dai', JWT_SECRET: 'test_secret', REDIS_URL: 'redis://localhost:6379/1' } } }); ``` ### 测试数据库设置 ```typescript // tests/fixtures/test-db.ts import { DataSource } from 'typeorm'; import { User } from '@d8d/user-module/entities'; import { File } from '@d8d/file-module/entities'; export async function getTestDataSource(): Promise { return new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'test_password', database: 'test_d8dai', entities: [User, File], synchronize: true, // 测试环境自动同步表结构 dropSchema: true, // 每次测试前清空数据库 logging: false }); } ``` ## 覆盖率标准 | 测试类型 | 最低要求 | 目标要求 | |----------|----------|----------| | 集成测试 | 50% | 60% | **关键端点要求**: - 认证端点:100%覆盖 - 用户管理端点:80%覆盖 - 文件上传端点:70%覆盖 ## 运行测试 ### 本地开发 ```bash # 运行所有集成测试 cd packages/server && pnpm test # 运行特定集成测试 pnpm test users.integration.test.ts # 生成覆盖率报告 pnpm test:coverage # 运行特定测试用例 pnpm test --testNamePattern="应该成功创建用户" ``` ### CI/CD ```yaml server-integration-tests: runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_d8dai options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 - run: cd packages/server && pnpm install - run: cd packages/server && pnpm test ``` ## 调试技巧 ### 1. 使用调试模式 ```bash # 运行特定测试并显示详细信息 pnpm test --testNamePattern="用户登录" --reporter=verbose # 监听模式(开发时) pnpm test --watch ``` ### 2. 查看响应详情 ```typescript const res = await integrateRoutes(app).POST('/api/users', { json: userData }); // 打印完整响应 console.log('Status:', res.status); console.log('Headers:', res.headers); console.log('Body:', await res.json()); ``` ### 3. 数据库调试 ```typescript // 启用SQL查询日志 const dataSource = new DataSource({ // ...其他配置 logging: true, // 显示所有SQL查询 maxQueryExecutionTime: 1000 // 记录慢查询 }); ``` ## 常见错误避免 ### ❌ 不要在集成测试中使用mock ```typescript // 错误:模拟数据库查询 vi.mock('@d8d/user-module/services/user.service', () => ({ UserService: { findAll: vi.fn(() => Promise.resolve(mockUsers)) } })); // 正确:使用真实的数据库和服务 ``` ### ❌ 不要忽略异步清理 ```typescript // 错误:不清理数据库 afterEach(() => { // 数据库没有被清理 }); // 正确:确保数据库清理 afterEach(async () => { await dataSource.dropDatabase(); }); ``` ### ❌ 不要硬编码测试数据 ```typescript // 错误:硬编码ID it('应该返回用户详情', async () => { const res = await integrateRoutes(app).GET('/api/users/123'); }); // 正确:使用动态创建的数据 it('应该返回用户详情', async () => { const user = await seedTestUser(dataSource); const res = await integrateRoutes(app).GET(`/api/users/${user.id}`); }); ``` ## 参考实现 - Server包集成测试:`packages/server/tests/integration/` - 共享测试工具:`packages/shared-test-util/` ## 相关文档 - [测试策略概述](./testing-strategy.md) - [后端模块包测试规范](./backend-module-testing-standards.md) - [Web UI包测试规范](./web-ui-testing-standards.md) - [后端模块包开发规范](./backend-module-package-standards.md) --- **文档状态**: 正式版 **适用范围**: packages/server