web-server-testing-standards.md 13 KB

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端点

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. 测试数据库集成

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. 测试认证中间件

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. 测试模块间集成

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. 测试文件上传集成

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. 测试错误处理

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. 使用共享测试工具

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);
  });
});

测试数据管理

测试数据工厂

// tests/fixtures/test-data.ts
import { User } from '@d8d/user-module/entities';

export function createTestUser(overrides = {}): Partial<User> {
  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);
}

数据库清理策略

// 选项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

测试描述

describe('用户管理API', () => {
  describe('GET /api/users', () => {
    it('应该返回用户列表', async () => { });
    it('应该支持分页', async () => { });
    it('应该支持搜索过滤', async () => { });
  });

  describe('POST /api/users', () => {
    it('应该创建新用户', async () => { });
    it('应该验证重复用户名', async () => { });
    it('应该验证邮箱格式', async () => { });
  });
});

环境配置

测试环境变量

// 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'
    }
  }
});

测试数据库设置

// 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<DataSource> {
  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%覆盖

运行测试

本地开发

# 运行所有集成测试
cd packages/server && pnpm test

# 运行特定集成测试
pnpm test users.integration.test.ts

# 生成覆盖率报告
pnpm test:coverage

# 运行特定测试用例
pnpm test --testNamePattern="应该成功创建用户"

CI/CD

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. 使用调试模式

# 运行特定测试并显示详细信息
pnpm test --testNamePattern="用户登录" --reporter=verbose

# 监听模式(开发时)
pnpm test --watch

2. 查看响应详情

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. 数据库调试

// 启用SQL查询日志
const dataSource = new DataSource({
  // ...其他配置
  logging: true, // 显示所有SQL查询
  maxQueryExecutionTime: 1000 // 记录慢查询
});

常见错误避免

❌ 不要在集成测试中使用mock

// 错误:模拟数据库查询
vi.mock('@d8d/user-module/services/user.service', () => ({
  UserService: {
    findAll: vi.fn(() => Promise.resolve(mockUsers))
  }
}));

// 正确:使用真实的数据库和服务

❌ 不要忽略异步清理

// 错误:不清理数据库
afterEach(() => {
  // 数据库没有被清理
});

// 正确:确保数据库清理
afterEach(async () => {
  await dataSource.dropDatabase();
});

❌ 不要硬编码测试数据

// 错误:硬编码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/

相关文档


文档状态: 正式版 适用范围: packages/server