| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2025-12-26 | 从测试策略文档拆分,专注后端模块包测试 | James (Claude Code) |
本文档定义了后端模块包的测试标准和最佳实践。
packages/*-module - 业务模块包(user-module、auth-module、file-module等)packages/*-module/tests/unit/**/*.test.tspackages/*-module/tests/integration/**/*.test.tspackages/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 # 测试数据工厂
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<Repository<User>>;
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<User>);
});
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('用户名已存在');
});
});
});
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();
});
});
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);
});
});
});
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('用户名已存在');
});
});
});
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();
});
});
});
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);
});
});
import { User } from '../src/entities/user.entity';
import { Repository } from 'typeorm';
export function buildUser(overrides = {}): Partial<User> {
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<User>,
overrides = {}
): Promise<User> {
const userData = buildUser(overrides);
const user = userRepo.create(userData);
return await userRepo.save(user);
}
// 批量创建测试数据
export async function createTestUsers(
userRepo: Repository<User>,
count: number,
overrides = {}
): Promise<User[]> {
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;
}
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);
// 测试结束后自动回滚,数据库为空
});
});
// 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<DataSource> {
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% |
关键模块定义:
# 运行所有测试
cd packages/user-module && pnpm test
# 只运行单元测试
pnpm test:unit
# 只运行集成测试
pnpm test:integration
# 生成覆盖率报告
pnpm test:coverage
# 运行特定测试文件
pnpm test user.service.test.ts
# 运行特定测试用例
pnpm test --testNamePattern="应该创建用户"
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.test.ts[schema].schema.test.ts[route].routes.integration.test.ts[service].service.integration.test.tsdescribe('UserService', () => {
describe('findById()', () => {
it('应该返回用户数据', async () => { });
it('应该返回null当用户不存在', async () => { });
});
describe('createUser()', () => {
it('应该创建新用户', async () => { });
it('应该拒绝重复的用户名', async () => { });
});
});
// 错误:单元测试连接真实数据库
it('应该查询用户', async () => {
const dataSource = await getTestDataSource(); // 不要在单元测试中
// ...
});
// 正确:使用模拟的Repository
it('应该查询用户', async () => {
const mockRepo = { findOne: vi.fn() };
const userService = new UserService(mockRepo);
// ...
});
// 错误:集成测试中mock数据库
vi.mock('typeorm', () => ({
getRepository: vi.fn(() => mockRepo)
}));
// 正确:使用真实数据库
const dataSource = await getTestDataSource();
const userRepo = dataSource.getRepository(User);
// 错误:测试间共享数据
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' });
});
const dataSource = new DataSource({
// ...
logging: true, // 显示所有SQL
maxQueryExecutionTime: 100 // 记录慢查询
});
it('调试测试', async () => {
const users = await userService.findAll();
console.debug('用户列表:', users);
console.debug('用户数量:', users.length);
});
it.only('只运行这个测试', async () => {
// ...
});
packages/user-module/tests/packages/auth-module/tests/packages/file-module/tests/文档状态: 正式版 适用范围: packages/*-module