| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 2.0 | 2025-12-26 | 基于实际测试实现重写,修正不准确的描述 | James (Claude Code) |
| 1.0 | 2025-12-26 | 从测试策略文档拆分,专注后端模块包测试 | James (Claude Code) |
本文档定义了后端模块包的测试标准和最佳实践,基于项目实际的测试实现经验总结。
重要: 项目的测试采用集中式管理模式:
tests/ 目录@d8d/shared-test-utilfileParallelism: false) 避免数据库连接冲突testClient - API路由测试IntegrationTestDatabase - 集成测试数据库管理setupIntegrationDatabaseHooksWithEntities - 测试生命周期钩子TestDataFactory - 测试数据工厂packages/core-module/
├── auth-module/
│ └── tests/
│ ├── integration/
│ │ ├── auth.integration.test.ts
│ │ ├── phone-decrypt.integration.test.ts
│ │ └── system-config-integration.test.ts
│ └── unit/
│ └── mini-auth.service.test.ts
├── file-module/
│ └── tests/
│ ├── integration/
│ │ └── file.routes.integration.test.ts
│ └── unit/
│ └── file.service.test.ts
└── user-module/
└── tests/
├── integration/
│ ├── role.integration.test.ts
│ └── user.routes.integration.test.ts
└── utils/
├── integration-test-db.ts # 模块专用测试工具
└── integration-test-utils.ts
allin-packages/order-module/
├── src/
│ ├── routes/
│ ├── services/
│ ├── entities/
│ └── schemas/
├── tests/
│ ├── integration/
│ │ ├── order.integration.test.ts
│ │ └── talent-employment.integration.test.ts
│ └── utils/
│ └── test-data-factory.ts
└── vitest.config.ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'tests/',
'**/*.d.ts',
'**/*.config.*'
]
},
// 关键: 关闭并行测试避免数据库连接冲突
fileParallelism: false
}
});
import { describe, it, expect, beforeEach } from 'vitest';
import { testClient } from 'hono/testing';
import {
IntegrationTestDatabase,
setupIntegrationDatabaseHooksWithEntities
} from '@d8d/shared-test-util';
import { userRoutes } from '../src/routes';
import { UserEntity } from '../src/entities/user.entity';
import { Role } from '../src/entities/role.entity';
import { File } from '@d8d/core-module/file-module';
import { TestDataFactory } from './utils/integration-test-db';
// 设置集成测试钩子
setupIntegrationDatabaseHooksWithEntities([UserEntity, Role, File])
describe('用户路由API集成测试', () => {
let client: ReturnType<typeof testClient<typeof userRoutes>>;
let authService: AuthService;
let testToken: string;
let testUser: any;
beforeEach(async () => {
// 创建测试客户端
client = testClient(userRoutes);
// 获取数据源
const dataSource = await IntegrationTestDatabase.getDataSource();
if (!dataSource) throw new Error('Database not initialized');
// 初始化服务
const userService = new UserService(dataSource);
authService = new AuthService(userService);
// 创建测试用户并生成token
testUser = await TestDataFactory.createTestUser(dataSource, {
username: 'testuser_auth',
email: 'testuser_auth@example.com'
});
testToken = authService.generateToken(testUser);
});
describe('用户创建路由测试', () => {
it('应该拒绝无认证令牌的用户创建请求', async () => {
const userData = {
username: 'testuser_create',
email: 'testcreate@example.com',
password: 'TestPassword123!',
nickname: 'Test User',
phone: '13800138001'
};
const response = await client.index.$post({
json: userData
});
// 应该返回401状态码,因为缺少认证
expect(response.status).toBe(401);
if (response.status === 401) {
const responseData = await response.json();
expect(responseData.message).toContain('Authorization header missing');
}
});
it('应该成功创建用户(使用有效认证令牌)', async () => {
const userData = {
username: 'testuser_create_success',
email: 'testcreate_success@example.com',
password: 'TestPassword123!',
nickname: 'Test User Success'
};
const response = await client.index.$post({
json: userData
}, {
headers: {
'Authorization': `Bearer ${testToken}`
}
});
expect(response.status).toBe(200);
const responseData = await response.json();
expect(responseData.data).toHaveProperty('id');
expect(responseData.data.username).toBe('testuser_create_success');
});
});
describe('用户查询路由测试', () => {
it('应该支持分页查询用户列表', async () => {
// 创建多个测试用户
const dataSource = await IntegrationTestDatabase.getDataSource();
for (let i = 0; i < 15; i++) {
await TestDataFactory.createTestUser(dataSource, {
username: `pageuser_${i}`
});
}
const response = await client.index.$get({
query: {
page: '1',
pageSize: '10'
}
}, {
headers: {
'Authorization': `Bearer ${testToken}`
}
});
expect(response.status).toBe(200);
const responseData = await response.json();
expect(responseData.data).toHaveLength(10);
expect(responseData.total).toBeGreaterThanOrEqual(15);
});
});
});
// tests/utils/integration-test-db.ts
import { DataSource } from 'typeorm';
import { UserEntity } from '../../src/entities/user.entity';
import { Role } from '../../src/entities/role.entity';
/**
* 测试数据工厂类
*/
export class TestDataFactory {
/**
* 创建测试用户数据
*/
static createUserData(overrides: Partial<UserEntity> = {}): Partial<UserEntity> {
const timestamp = Date.now();
return {
username: `testuser_${timestamp}`,
password: 'TestPassword123!',
email: `test_${timestamp}@example.com`,
phone: `138${timestamp.toString().slice(-8)}`,
nickname: `Test User ${timestamp}`,
name: `Test Name ${timestamp}`,
isDisabled: 0,
isDeleted: 0,
...overrides
};
}
/**
* 创建测试角色数据
*/
static createRoleData(overrides: Partial<Role> = {}): Partial<Role> {
const timestamp = Date.now();
return {
name: `test_role_${timestamp}`,
description: `Test role description ${timestamp}`,
...overrides
};
}
/**
* 在数据库中创建测试用户
*/
static async createTestUser(
dataSource: DataSource,
overrides: Partial<UserEntity> = {}
): Promise<UserEntity> {
const userData = this.createUserData(overrides);
const userRepository = dataSource.getRepository(UserEntity);
const user = userRepository.create(userData);
return await userRepository.save(user);
}
/**
* 在数据库中创建测试角色
*/
static async createTestRole(
dataSource: DataSource,
overrides: Partial<Role> = {}
): Promise<Role> {
const roleData = this.createRoleData(overrides);
const roleRepository = dataSource.getRepository(Role);
const role = roleRepository.create(roleData);
return await roleRepository.save(role);
}
}
// tests/utils/integration-test-utils.ts
import { IntegrationTestDatabase } from '@d8d/shared-test-util';
import { UserEntity } from '../../src/entities/user.entity';
/**
* 集成测试断言工具
*/
export class IntegrationTestAssertions {
/**
* 断言响应状态码
*/
static expectStatus(response: { status: number }, expectedStatus: number): void {
if (response.status !== expectedStatus) {
throw new Error(`Expected status ${expectedStatus}, but got ${response.status}`);
}
}
/**
* 断言用户存在于数据库中
*/
static async expectUserToExist(username: string): Promise<void> {
const dataSource = await IntegrationTestDatabase.getDataSource();
if (!dataSource) {
throw new Error('Database not initialized');
}
const userRepository = dataSource.getRepository(UserEntity);
const user = await userRepository.findOne({ where: { username } });
if (!user) {
throw new Error(`Expected user ${username} to exist in database`);
}
}
/**
* 断言用户不存在于数据库中
*/
static async expectUserNotToExist(username: string): Promise<void> {
const dataSource = await IntegrationTestDatabase.getDataSource();
if (!dataSource) {
throw new Error('Database not initialized');
}
const userRepository = dataSource.getRepository(UserEntity);
const user = await userRepository.findOne({ where: { username } });
if (user) {
throw new Error(`Expected user ${username} not to exist in database`);
}
}
}
describe('订单管理完整流程测试', () => {
let client: ReturnType<typeof testClient<typeof orderRoutes>>;
let testToken: string;
let testPlatform: any;
let testCompany: any;
let testChannel: any;
beforeEach(async () => {
client = testClient(orderRoutes);
const dataSource = await IntegrationTestDatabase.getDataSource();
// 创建测试基础数据
testPlatform = await createTestPlatform(dataSource);
testCompany = await createTestCompany(dataSource);
testChannel = await createTestChannel(dataSource);
// 创建测试用户并生成token
const testUser = await TestDataFactory.createTestUser(dataSource);
testToken = JWTUtil.generateToken({
userId: testUser.id,
username: testUser.username
});
});
it('应该完成订单创建到分配人员的完整流程', async () => {
// 1. 创建订单
const createResponse = await client.create.$post({
json: {
orderName: '测试订单',
platformId: testPlatform.id,
companyId: testCompany.id,
channelId: testChannel.id,
expectedStartDate: new Date().toISOString(),
orderStatus: 'DRAFT'
}
}, {
headers: { 'Authorization': `Bearer ${testToken}` }
});
expect(createResponse.status).toBe(200);
const { data: order } = await createResponse.json();
// 2. 更新订单状态
const updateResponse = await client[':id'].$patch({
param: { id: order.id },
json: { orderStatus: 'ACTIVE' }
}, {
headers: { 'Authorization': `Bearer ${testToken}` }
});
expect(updateResponse.status).toBe(200);
// 3. 分配人员到订单
const assignResponse = await client.assign.$post({
json: {
orderId: order.id,
personIds: [1, 2, 3]
}
}, {
headers: { 'Authorization': `Bearer ${testToken}` }
});
expect(assignResponse.status).toBe(200);
// 4. 验证订单详情
const detailResponse = await client[':id'].$get({
param: { id: order.id }
}, {
headers: { 'Authorization': `Bearer ${testToken}` }
});
expect(detailResponse.status).toBe(200);
const { data: orderDetail } = await detailResponse.json();
expect(orderDetail.orderStatus).toBe('ACTIVE');
});
});
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { DataSource } from 'typeorm';
import { FileService } from '../../src/services/file.service';
import { File } from '../../src/entities/file.entity';
import { MinioService } from '../../src/services/minio.service';
// Mock依赖项
vi.mock('../../src/services/minio.service');
vi.mock('@d8d/shared-utils', () => ({
logger: {
error: vi.fn(),
db: vi.fn()
},
ErrorSchema: {}
}));
vi.mock('uuid', () => ({
v4: () => 'test-uuid-123'
}));
describe('FileService', () => {
let mockDataSource: DataSource;
let fileService: FileService;
beforeEach(() => {
mockDataSource = {
getRepository: vi.fn(() => ({
findOne: vi.fn(),
findOneBy: vi.fn(),
save: vi.fn(),
create: vi.fn()
}))
} as unknown as DataSource;
fileService = new FileService(mockDataSource);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('createFile', () => {
it('应该成功创建文件并生成上传策略', async () => {
const mockFileData = {
name: 'test.txt',
type: 'text/plain',
size: 1024,
uploadUserId: 1
};
const mockUploadPolicy = {
'x-amz-algorithm': 'test-algorithm',
'x-amz-credential': 'test-credential',
host: 'https://minio.example.com'
};
const mockSavedFile = {
id: 1,
...mockFileData,
path: '1/test-uuid-123-test.txt',
uploadTime: new Date(),
createdAt: new Date(),
updatedAt: new Date()
};
const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy);
vi.mocked(MinioService).mockImplementation(() => ({
generateUploadPolicy: mockGenerateUploadPolicy
} as unknown as MinioService));
// Mock GenericCrudService的create方法
vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File);
const result = await fileService.createFile(mockFileData);
expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt');
expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({
name: 'test.txt',
path: '1/test-uuid-123-test.txt',
uploadUserId: 1
}));
expect(result).toEqual({
file: mockSavedFile,
uploadPolicy: mockUploadPolicy
});
});
it('应该处理文件创建错误', async () => {
const mockFileData = {
name: 'test.txt',
uploadUserId: 1
};
const mockGenerateUploadPolicy = vi.fn()
.mockRejectedValue(new Error('MinIO error'));
vi.mocked(MinioService).mockImplementation(() => ({
generateUploadPolicy: mockGenerateUploadPolicy
} as unknown as MinioService));
await expect(fileService.createFile(mockFileData))
.rejects.toThrow('MinIO error');
});
});
});
import { describe, it, expect } from 'vitest';
import { CreatePlatformSchema, UpdatePlatformSchema } from '../../src/schemas/platform.schema';
describe('平台Schema验证测试', () => {
describe('CreatePlatformSchema', () => {
it('应该验证有效的平台数据', () => {
const data = {
platformName: '测试平台',
contactEmail: 'test@example.com',
contactPhone: '13800138000'
};
const result = CreatePlatformSchema.safeParse(data);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.platformName).toBe('测试平台');
expect(result.data.contactEmail).toBe('test@example.com');
}
});
it('应该验证空字符串转换为undefined', () => {
const data = {
platformName: '测试平台',
contactEmail: '' // 空字符串应该转换为undefined
};
const result = CreatePlatformSchema.safeParse(data);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.contactEmail).toBeUndefined();
}
});
it('应该拒绝无效的邮箱格式', () => {
const data = {
platformName: '测试平台',
contactEmail: 'invalid-email'
};
const result = CreatePlatformSchema.safeParse(data);
expect(result.success).toBe(false);
});
it('应该拒绝缺失必填字段', () => {
const data = {
contactEmail: 'test@example.com'
// 缺少 platformName
};
const result = CreatePlatformSchema.safeParse(data);
expect(result.success).toBe(false);
});
});
});
import { describe, it, expect } from 'vitest';
import { generateOrderNumber, calculateOrderAmount } from '../../src/utils/order.util';
describe('订单工具函数测试', () => {
describe('generateOrderNumber', () => {
it('应该生成唯一订单号', () => {
const orderNumber1 = generateOrderNumber();
const orderNumber2 = generateOrderNumber();
expect(orderNumber1).not.toBe(orderNumber2);
expect(orderNumber1).toMatch(/^ORD\d{13}$/); // ORD + 13位时间戳
});
it('应该生成带前缀的订单号', () => {
const orderNumber = generateOrderNumber('TEST');
expect(orderNumber).toMatch(/^TEST\d{13}$/);
});
});
describe('calculateOrderAmount', () => {
it('应该正确计算订单金额', () => {
const items = [
{ quantity: 2, unitPrice: 100 },
{ quantity: 3, unitPrice: 50 }
];
const total = calculateOrderAmount(items);
expect(total).toBe(350); // 2*100 + 3*50
});
it('应该处理空数组', () => {
const total = calculateOrderAmount([]);
expect(total).toBe(0);
});
it('应该处理小数精度', () => {
const items = [
{ quantity: 1, unitPrice: 99.99 },
{ quantity: 2, unitPrice: 50.005 }
];
const total = calculateOrderAmount(items);
expect(total).toBeCloseTo(200, 2);
});
});
});
来自 @d8d/shared-test-util,用于管理集成测试数据库:
import {
IntegrationTestDatabase,
setupIntegrationDatabaseHooksWithEntities
} from '@d8d/shared-test-util';
import { UserEntity } from '../src/entities/user.entity';
import { Role } from '../src/entities/role.entity';
// 设置测试生命周期钩子
setupIntegrationDatabaseHooksWithEntities([UserEntity, Role])
describe('集成测试', () => {
it('使用集成测试数据库', async () => {
// 获取数据源
const dataSource = await IntegrationTestDatabase.getDataSource();
// 使用Repository
const userRepo = dataSource.getRepository(UserEntity);
const user = await userRepo.findOne({ where: { id: 1 } });
expect(user).toBeDefined();
});
});
每个模块可以定义自己的测试工具:
// tests/utils/integration-test-db.ts
import { DataSource } from 'typeorm';
import { IntegrationTestDatabase } from '@d8d/shared-test-util';
import { Order } from '../../src/entities/order.entity';
import { Talent } from '../../src/entities/talent.entity';
export class OrderTestDataFactory {
/**
* 创建测试订单数据
*/
static async createTestOrder(
dataSource: DataSource,
overrides: Partial<Order> = {}
): Promise<Order> {
const timestamp = Date.now();
const orderData = {
orderName: `测试订单_${timestamp}`,
orderStatus: 'DRAFT',
workStatus: 'NOT_WORKING',
...overrides
};
const orderRepo = dataSource.getRepository(Order);
const order = orderRepo.create(orderData);
return await orderRepo.save(order);
}
/**
* 创建测试残疾人数据
*/
static async createTestTalent(
dataSource: DataSource,
overrides: Partial<Talent> = {}
): Promise<Talent> {
const timestamp = Date.now();
const talentData = {
name: `测试残疾人_${timestamp}`,
idCard: `110101199001011${timestamp.toString().slice(-3)}`,
phone: `138${timestamp.toString().slice(-8)}`,
...overrides
};
const talentRepo = dataSource.getRepository(Talent);
const talent = talentRepo.create(talentData);
return await talentRepo.save(talent);
}
}
[feature].integration.test.ts[component].test.tsintegration-test-db.ts、integration-test-utils.tsdescribe('[模块名]', () => {
describe('[功能名]', () => {
it('应该[预期行为]', async () => { });
it('应该拒绝[错误情况]', async () => { });
});
});
# 进入模块目录
cd packages/core-module/user-module
# 或
cd allin-packages/order-module
# 运行所有测试
pnpm test
# 运行集成测试
pnpm test:integration
# 运行单元测试
pnpm test:unit
# 生成覆盖率报告
pnpm test:coverage
# 运行特定测试
pnpm test --testNamePattern="应该成功创建用户"
# 监听模式
pnpm test --watch
# 运行所有模块测试
pnpm test
# 运行特定目录的测试
pnpm test "packages/core-module/**/*.test.ts"
pnpm test "allin-packages/**/*.test.ts"
| 测试类型 | 最低要求 | 目标要求 | 关键模块要求 |
|---|---|---|---|
| 集成测试 | 60% | 70% | 80% |
| 单元测试 | 50% | 60% | 70% |
关键模块定义:
// 错误:集成测试中mock数据库Repository
vi.mock('typeorm', () => ({
getRepository: vi.fn(() => mockRepo)
}));
// 正确:使用真实的数据库和Repository
const dataSource = await IntegrationTestDatabase.getDataSource();
const userRepo = dataSource.getRepository(UserEntity);
// 错误:硬编码用户名
it('应该创建用户', async () => {
await createTestUser(dataSource, { username: 'testuser' });
});
// 正确:使用时间戳生成唯一数据
it('应该创建用户', async () => {
await TestDataFactory.createTestUser(dataSource); // 自动生成唯一用户名
});
// 错误:不测试认证
it('应该创建用户', async () => {
const response = await client.create.$post({ json: userData });
expect(response.status).toBe(200);
});
// 正确:测试有认证和无认证两种情况
it('应该拒绝无认证的请求', async () => {
const response = await client.create.$post({ json: userData });
expect(response.status).toBe(401);
});
it('应该接受有效认证的请求', async () => {
const response = await client.create.$post({
json: userData
}, {
headers: { 'Authorization': `Bearer ${testToken}` }
});
expect(response.status).toBe(200);
});
// 错误:不使用setupIntegrationDatabaseHooksWithEntities
describe('测试', () => {
beforeEach(async () => {
await IntegrationTestDatabase.initializeWithEntities([UserEntity]);
});
// 没有afterEach清理
});
// 正确:使用setupIntegrationDatabaseHooksWithEntities自动管理
setupIntegrationDatabaseHooksWithEntities([UserEntity])
describe('测试', () => {
// beforeEach和afterEach自动设置
});
# 运行特定测试文件
pnpm test user.routes.integration.test.ts
# 运行匹配名称的测试
pnpm test --testNamePattern="应该成功创建用户"
# 显示详细输出
pnpm test --reporter=verbose
// 在测试中启用SQL日志
const dataSource = await IntegrationTestDatabase.getDataSource();
// 查看实际执行的SQL
dataSource.driver.createQueryRunner('debug').query('SELECT * FROM users');
describe.only('专注这个测试套件', () => {
it.only('只运行这个测试', async () => {
// ...
});
});
packages/core-module/user-module/tests/packages/core-module/auth-module/tests/packages/core-module/file-module/tests/allin-packages/order-module/tests/allin-packages/company-module/tests/allin-packages/channel-module/tests/allin-packages/disability-module/tests/packages/shared-test-util/src/文档状态: 正式版 适用范围: packages/core-module、allin-packages/ 基于实际测试实现: 2025-12-26