| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2025-12-26 | 从测试策略文档拆分,专注Web Server包测试 | James (Claude Code) |
本文档定义了Web Server包的测试标准和最佳实践。
packages/server - API服务器包packages/server/tests/integration/**/*.test.tspackages/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 # 测试数据工厂
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');
});
});
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');
});
});
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');
});
});
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');
});
});
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');
});
});
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');
});
});
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[endpoint].integration.test.tsdescribe('用户管理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% |
关键端点要求:
# 运行所有集成测试
cd packages/server && pnpm test
# 运行特定集成测试
pnpm test users.integration.test.ts
# 生成覆盖率报告
pnpm test:coverage
# 运行特定测试用例
pnpm test --testNamePattern="应该成功创建用户"
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
# 运行特定测试并显示详细信息
pnpm test --testNamePattern="用户登录" --reporter=verbose
# 监听模式(开发时)
pnpm test --watch
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());
// 启用SQL查询日志
const dataSource = new DataSource({
// ...其他配置
logging: true, // 显示所有SQL查询
maxQueryExecutionTime: 1000 // 记录慢查询
});
// 错误:模拟数据库查询
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}`);
});
packages/server/tests/integration/packages/shared-test-util/文档状态: 正式版 适用范围: packages/server