29 KiB
29 KiB
MPC Party Service 测试文档
概述
本文档详细说明 MPC Party Service 的测试架构、测试策略和实现方式。
测试金字塔
┌─────────┐
│ E2E │ ← 端到端测试 (15 tests)
┌┴─────────┴┐
│Integration │ ← 集成测试 (30 tests)
┌┴───────────┴┐
│ Unit Tests │ ← 单元测试 (81 tests)
└───────────────┘
总计: 111+ 测试用例
测试分类
1. 单元测试 (Unit Tests)
测试单个组件的隔离行为,不依赖外部服务。
目录: tests/unit/
运行命令:
npm run test:unit
覆盖范围:
- 领域实体 (Domain Entities)
- 值对象 (Value Objects)
- 领域服务 (Domain Services)
- 应用层处理器 (Application Handlers)
- 数据映射器 (Data Mappers)
2. 集成测试 (Integration Tests)
测试多个组件之间的交互,使用模拟的外部依赖。
目录: tests/integration/
运行命令:
npm run test:integration
覆盖范围:
- 仓储实现 (Repository Implementations)
- 控制器 (Controllers)
- 事件发布器 (Event Publishers)
3. 端到端测试 (E2E Tests)
测试完整的 API 流程,模拟真实的客户端请求。
目录: tests/e2e/
运行命令:
npm run test:e2e
覆盖范围:
- API 端点
- 认证流程
- 错误处理
- 完整的请求/响应周期
测试配置
Jest 配置文件
基础配置 (tests/jest.config.js)
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '..',
testEnvironment: 'node',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.module.ts',
'!src/main.ts',
],
coverageDirectory: './coverage',
testMatch: [
'<rootDir>/tests/unit/**/*.spec.ts',
'<rootDir>/tests/integration/**/*.spec.ts',
],
};
单元测试配置 (tests/jest-unit.config.js)
const baseConfig = require('./jest.config');
module.exports = {
...baseConfig,
testMatch: ['<rootDir>/tests/unit/**/*.spec.ts'],
testTimeout: 30000,
};
集成测试配置 (tests/jest-integration.config.js)
const baseConfig = require('./jest.config');
module.exports = {
...baseConfig,
testMatch: ['<rootDir>/tests/integration/**/*.spec.ts'],
testTimeout: 60000,
};
E2E 测试配置 (tests/jest-e2e.config.js)
const baseConfig = require('./jest.config');
module.exports = {
...baseConfig,
testMatch: ['<rootDir>/tests/e2e/**/*.e2e-spec.ts'],
testTimeout: 120000,
maxWorkers: 1, // 顺序执行
};
测试环境设置 (tests/setup.ts)
export {};
// 设置测试环境变量
process.env.NODE_ENV = 'test';
process.env.APP_PORT = '3006';
process.env.DATABASE_URL = 'mysql://test:test@localhost:3306/test_db';
process.env.REDIS_HOST = 'localhost';
process.env.REDIS_PORT = '6379';
process.env.JWT_SECRET = 'test-jwt-secret-key-for-testing-only';
process.env.SHARE_MASTER_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
process.env.MPC_PARTY_ID = 'party-test-1';
process.env.KAFKA_ENABLED = 'false';
// 增加异步操作超时
jest.setTimeout(30000);
// 自定义匹配器
expect.extend({
toBeValidUUID(received: string) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const pass = uuidRegex.test(received);
return {
message: () => pass
? `expected ${received} not to be a valid UUID`
: `expected ${received} to be a valid UUID`,
pass,
};
},
toBeValidHex(received: string, expectedLength?: number) {
const hexRegex = /^[0-9a-f]+$/i;
const isHex = hexRegex.test(received);
const lengthMatch = expectedLength === undefined || received.length === expectedLength;
return {
message: () => `expected ${received} to be valid hex`,
pass: isHex && lengthMatch,
};
},
toBeValidPublicKey(received: string) {
const hexRegex = /^[0-9a-f]+$/i;
const isHex = hexRegex.test(received);
const isValidLength = received.length === 66 || received.length === 130;
return {
message: () => `expected ${received} to be a valid public key`,
pass: isHex && isValidLength,
};
},
});
单元测试详解
领域实体测试
PartyShare 实体 (tests/unit/domain/party-share.entity.spec.ts)
describe('PartyShare Entity', () => {
const createTestShareData = () => ShareData.create(
Buffer.from('encrypted-share-data'),
Buffer.from('123456789012'), // 12 bytes IV
Buffer.from('1234567890123456'), // 16 bytes authTag
);
describe('create', () => {
it('should create a valid party share', () => {
const share = PartyShare.create({
partyId: PartyId.create('user123-server'),
sessionId: SessionId.generate(),
shareType: PartyShareType.WALLET,
shareData: createTestShareData(),
publicKey: PublicKey.fromHex('03' + '0'.repeat(64)),
threshold: Threshold.create(3, 2),
});
expect(share).toBeDefined();
expect(share.id).toBeDefined();
expect(share.status).toBe(PartyShareStatus.ACTIVE);
});
it('should emit ShareCreatedEvent', () => {
const share = PartyShare.create({/*...*/});
const events = share.domainEvents;
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(ShareCreatedEvent);
});
});
describe('markAsUsed', () => {
it('should update lastUsedAt timestamp', () => {
const share = PartyShare.create({/*...*/});
const before = share.lastUsedAt;
share.markAsUsed();
expect(share.lastUsedAt).toBeDefined();
expect(share.lastUsedAt).not.toBe(before);
});
it('should throw error when share is not active', () => {
const share = PartyShare.create({/*...*/});
share.revoke('test reason');
expect(() => share.markAsUsed()).toThrow(DomainError);
});
});
describe('rotate', () => {
it('should create new share with rotated status for old share', () => {
const oldShare = PartyShare.create({/*...*/});
const newShareData = createTestShareData();
const newShare = oldShare.rotate(newShareData, SessionId.generate());
expect(oldShare.status).toBe(PartyShareStatus.ROTATED);
expect(newShare.status).toBe(PartyShareStatus.ACTIVE);
expect(newShare.publicKey.toHex()).toBe(oldShare.publicKey.toHex());
});
});
describe('revoke', () => {
it('should mark share as revoked', () => {
const share = PartyShare.create({/*...*/});
share.revoke('Security concern');
expect(share.status).toBe(PartyShareStatus.REVOKED);
});
});
});
值对象测试
Value Objects (tests/unit/domain/value-objects.spec.ts)
describe('Value Objects', () => {
describe('SessionId', () => {
it('should create valid SessionId', () => {
const sessionId = SessionId.create('550e8400-e29b-41d4-a716-446655440000');
expect(sessionId.value).toBe('550e8400-e29b-41d4-a716-446655440000');
});
it('should generate unique SessionId', () => {
const id1 = SessionId.generate();
const id2 = SessionId.generate();
expect(id1.value).not.toBe(id2.value);
});
it('should throw error for invalid format', () => {
expect(() => SessionId.create('invalid')).toThrow();
});
});
describe('Threshold', () => {
it('should create valid threshold', () => {
const threshold = Threshold.create(3, 2);
expect(threshold.n).toBe(3);
expect(threshold.t).toBe(2);
});
it('should validate participants', () => {
const threshold = Threshold.create(3, 2);
expect(threshold.canSign(2)).toBe(true);
expect(threshold.canSign(1)).toBe(false);
});
it('should throw error for invalid values', () => {
expect(() => Threshold.create(2, 3)).toThrow(); // t > n
expect(() => Threshold.create(0, 1)).toThrow(); // n = 0
});
});
describe('ShareData', () => {
it('should serialize to JSON and back', () => {
const original = ShareData.create(
Buffer.from('data'),
Buffer.from('123456789012'),
Buffer.from('1234567890123456'),
);
const json = original.toJSON();
const restored = ShareData.fromJSON(json);
expect(restored.encryptedData).toEqual(original.encryptedData);
});
});
});
领域服务测试
ShareEncryptionDomainService (tests/unit/domain/share-encryption.spec.ts)
describe('ShareEncryptionDomainService', () => {
let service: ShareEncryptionDomainService;
let masterKey: Buffer;
beforeEach(() => {
service = new ShareEncryptionDomainService();
masterKey = service.generateMasterKey();
});
describe('encrypt/decrypt', () => {
it('should encrypt and decrypt data correctly', () => {
const plaintext = Buffer.from('secret share data');
const encrypted = service.encrypt(plaintext, masterKey);
const decrypted = service.decrypt(encrypted, masterKey);
expect(decrypted).toEqual(plaintext);
});
it('should produce different ciphertexts for same plaintext', () => {
const plaintext = Buffer.from('secret share data');
const encrypted1 = service.encrypt(plaintext, masterKey);
const encrypted2 = service.encrypt(plaintext, masterKey);
expect(encrypted1.encryptedData).not.toEqual(encrypted2.encryptedData);
});
it('should fail decryption with wrong key', () => {
const plaintext = Buffer.from('secret share data');
const wrongKey = service.generateMasterKey();
const encrypted = service.encrypt(plaintext, masterKey);
expect(() => service.decrypt(encrypted, wrongKey)).toThrow();
});
it('should fail decryption with tampered ciphertext', () => {
const plaintext = Buffer.from('secret share data');
const encrypted = service.encrypt(plaintext, masterKey);
encrypted.encryptedData[0] ^= 0xFF; // 篡改数据
expect(() => service.decrypt(encrypted, masterKey)).toThrow();
});
});
describe('deriveKeyFromPassword', () => {
it('should derive consistent key from password', async () => {
const password = 'test-password';
const salt = Buffer.from('1234567890123456');
const key1 = await service.deriveKeyFromPassword(password, salt);
const key2 = await service.deriveKeyFromPassword(password, salt);
expect(key1).toEqual(key2);
});
});
});
应用层处理器测试
ParticipateInKeygenHandler (tests/unit/application/participate-keygen.handler.spec.ts)
describe('ParticipateInKeygenHandler', () => {
let handler: ParticipateInKeygenHandler;
let mockPartyShareRepository: any;
let mockTssProtocolService: any;
let mockCoordinatorClient: any;
let mockEncryptionService: any;
let mockEventPublisher: any;
beforeEach(async () => {
// 创建模拟对象
mockPartyShareRepository = {
save: jest.fn(),
findById: jest.fn(),
};
mockTssProtocolService = {
runKeygen: jest.fn(),
};
mockCoordinatorClient = {
joinSession: jest.fn(),
reportCompletion: jest.fn(),
};
mockEncryptionService = {
encrypt: jest.fn(),
};
mockEventPublisher = {
publishAll: jest.fn(),
};
// 创建测试模块
const module: TestingModule = await Test.createTestingModule({
providers: [
ParticipateInKeygenHandler,
{ provide: PARTY_SHARE_REPOSITORY, useValue: mockPartyShareRepository },
{ provide: TSS_PROTOCOL_SERVICE, useValue: mockTssProtocolService },
{ provide: MPCCoordinatorClient, useValue: mockCoordinatorClient },
{ provide: ShareEncryptionDomainService, useValue: mockEncryptionService },
{ provide: EventPublisherService, useValue: mockEventPublisher },
// ... 其他依赖
],
}).compile();
handler = module.get<ParticipateInKeygenHandler>(ParticipateInKeygenHandler);
});
it('should be defined', () => {
expect(handler).toBeDefined();
});
describe('execute', () => {
it('should have properly constructed command', () => {
const command = new ParticipateInKeygenCommand(
'550e8400-e29b-41d4-a716-446655440000',
'user123-server',
'join-token-abc123',
PartyShareType.WALLET,
'user-id-123',
);
expect(command.sessionId).toBe('550e8400-e29b-41d4-a716-446655440000');
expect(command.partyId).toBe('user123-server');
expect(command.shareType).toBe(PartyShareType.WALLET);
});
});
describe('error handling', () => {
it('should handle coordinator connection failure', async () => {
mockCoordinatorClient.joinSession.mockRejectedValue(
new Error('Connection refused'),
);
expect(mockCoordinatorClient.joinSession).toBeDefined();
});
it('should handle TSS protocol errors', async () => {
mockTssProtocolService.runKeygen.mockRejectedValue(
new Error('TSS protocol failed'),
);
expect(mockTssProtocolService.runKeygen).toBeDefined();
});
});
});
集成测试详解
仓储集成测试
PartyShareRepository (tests/integration/party-share.repository.spec.ts)
describe('PartyShareRepository (Integration)', () => {
let repository: PartyShareRepositoryImpl;
let prismaService: any;
let mapper: PartyShareMapper;
const createMockShare = (): PartyShare => {
return PartyShare.create({
partyId: PartyId.create('user123-server'),
sessionId: SessionId.generate(),
shareType: PartyShareType.WALLET,
shareData: ShareData.create(
Buffer.from('encrypted-test-share-data'),
Buffer.from('123456789012'),
Buffer.from('1234567890123456'),
),
publicKey: PublicKey.fromHex('03' + '0'.repeat(64)),
threshold: Threshold.create(3, 2),
});
};
beforeEach(async () => {
// 模拟 Prisma Service
prismaService = {
partyShare: {
findUnique: jest.fn(),
findFirst: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
count: jest.fn(),
},
};
mapper = new PartyShareMapper();
const module: TestingModule = await Test.createTestingModule({
providers: [
PartyShareRepositoryImpl,
{ provide: PrismaService, useValue: prismaService },
PartyShareMapper,
],
}).compile();
repository = module.get<PartyShareRepositoryImpl>(PartyShareRepositoryImpl);
});
describe('save', () => {
it('should save a new share', async () => {
const share = createMockShare();
prismaService.partyShare.create.mockResolvedValue({
...mapper.toPersistence(share),
id: share.id.value,
});
await repository.save(share);
expect(prismaService.partyShare.create).toHaveBeenCalled();
});
});
describe('findById', () => {
it('should return share when found', async () => {
const share = createMockShare();
const persistenceData = mapper.toPersistence(share);
prismaService.partyShare.findUnique.mockResolvedValue({
...persistenceData,
id: share.id.value,
});
const result = await repository.findById(share.id);
expect(result).toBeDefined();
expect(result).toBeInstanceOf(PartyShare);
});
it('should return null when not found', async () => {
prismaService.partyShare.findUnique.mockResolvedValue(null);
const result = await repository.findById(ShareId.generate());
expect(result).toBeNull();
});
});
describe('delete', () => {
it('should soft delete share by updating status to revoked', async () => {
const shareId = ShareId.generate();
prismaService.partyShare.update.mockResolvedValue({});
await repository.delete(shareId);
expect(prismaService.partyShare.update).toHaveBeenCalledWith({
where: { id: shareId.value },
data: {
status: PartyShareStatus.REVOKED,
updatedAt: expect.any(Date),
},
});
});
});
});
控制器集成测试
MPCPartyController (tests/integration/mpc-party.controller.spec.ts)
describe('MPCPartyController (Integration)', () => {
let controller: MPCPartyController;
let mockApplicationService: any;
beforeEach(async () => {
mockApplicationService = {
participateInKeygen: jest.fn(),
participateInSigning: jest.fn(),
rotateShare: jest.fn(),
getShareInfo: jest.fn(),
listShares: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [MPCPartyController],
providers: [
{ provide: MPCPartyApplicationService, useValue: mockApplicationService },
{ provide: JwtService, useValue: { verifyAsync: jest.fn() } },
{ provide: ConfigService, useValue: { get: jest.fn() } },
{ provide: Reflector, useValue: { getAllAndOverride: jest.fn().mockReturnValue(true) } },
],
}).compile();
controller = module.get<MPCPartyController>(MPCPartyController);
});
describe('health', () => {
it('should return health status', () => {
const result = controller.health();
expect(result.status).toBe('ok');
expect(result.service).toBe('mpc-party-service');
expect(result.timestamp).toBeDefined();
});
});
describe('participateInKeygen', () => {
it('should return accepted response for async keygen', async () => {
mockApplicationService.participateInKeygen.mockReturnValue(new Promise(() => {}));
const dto = {
sessionId: '550e8400-e29b-41d4-a716-446655440000',
partyId: 'user123-server',
joinToken: 'join-token-abc',
shareType: PartyShareType.WALLET,
userId: 'user-id-123',
};
const result = await controller.participateInKeygen(dto);
expect(result).toEqual({
message: 'Keygen participation started',
sessionId: dto.sessionId,
partyId: dto.partyId,
});
});
});
describe('listShares', () => {
it('should call application service listShares', async () => {
const mockResult = {
items: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
};
mockApplicationService.listShares.mockResolvedValue(mockResult);
const result = await controller.listShares({ partyId: 'user123-server', page: 1, limit: 10 });
expect(mockApplicationService.listShares).toHaveBeenCalled();
expect(result).toEqual(mockResult);
});
});
});
端到端测试详解
E2E 测试 (tests/e2e/mpc-service.e2e-spec.ts)
describe('MPC Service E2E Tests', () => {
let app: INestApplication;
let prismaService: any;
let jwtService: JwtService;
let authToken: string;
// 模拟外部服务
let mockCoordinatorClient: any;
let mockMessageRouterClient: any;
let mockTssProtocolService: any;
let mockEventPublisher: any;
beforeAll(async () => {
// 设置模拟实现
mockCoordinatorClient = {
joinSession: jest.fn(),
reportCompletion: jest.fn(),
};
mockTssProtocolService = {
runKeygen: jest.fn(),
runSigning: jest.fn(),
runRefresh: jest.fn(),
};
mockEventPublisher = {
publish: jest.fn(),
publishAll: jest.fn(),
};
// 模拟 Prisma
prismaService = {
partyShare: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
count: jest.fn(),
},
// ...
};
// 创建测试应用
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(PrismaService)
.useValue(prismaService)
.overrideProvider(MPCCoordinatorClient)
.useValue(mockCoordinatorClient)
.overrideProvider(TSS_PROTOCOL_SERVICE)
.useValue(mockTssProtocolService)
.overrideProvider(EventPublisherService)
.useValue(mockEventPublisher)
.compile();
app = moduleFixture.createNestApplication();
app.setGlobalPrefix('api/v1');
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}));
await app.init();
jwtService = moduleFixture.get<JwtService>(JwtService);
// 生成测试令牌
authToken = jwtService.sign({
sub: 'test-user-id',
type: 'access',
partyId: 'user123-server',
});
});
afterAll(async () => {
await app.close();
});
describe('Health Check', () => {
it('GET /api/v1/mpc-party/health - should return healthy status', async () => {
const response = await request(app.getHttpServer())
.get('/api/v1/mpc-party/health')
.expect(200);
const body = response.body.data || response.body;
expect(body.status).toBe('ok');
expect(body.service).toBe('mpc-party-service');
});
});
describe('Keygen Flow', () => {
it('POST /api/v1/mpc-party/keygen/participate - should accept keygen participation', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/mpc-party/keygen/participate')
.set('Authorization', `Bearer ${authToken}`)
.send({
sessionId: '550e8400-e29b-41d4-a716-446655440000',
partyId: 'user123-server',
joinToken: 'join-token-abc',
shareType: PartyShareType.WALLET,
userId: 'test-user-id',
})
.expect(202);
const body = response.body.data || response.body;
expect(body.message).toBe('Keygen participation started');
});
it('should validate required fields', async () => {
await request(app.getHttpServer())
.post('/api/v1/mpc-party/keygen/participate')
.set('Authorization', `Bearer ${authToken}`)
.send({ sessionId: '550e8400-e29b-41d4-a716-446655440000' })
.expect(400);
});
});
describe('Authentication', () => {
it('should reject requests without token', async () => {
await request(app.getHttpServer())
.get('/api/v1/mpc-party/shares')
.expect(401);
});
it('should reject requests with invalid token', async () => {
await request(app.getHttpServer())
.get('/api/v1/mpc-party/shares')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
it('should reject requests with expired token', async () => {
const expiredToken = jwtService.sign(
{ sub: 'test-user', type: 'access' },
{ expiresIn: '-1h' },
);
await request(app.getHttpServer())
.get('/api/v1/mpc-party/shares')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});
describe('Error Handling', () => {
it('should return structured error for validation failures', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/mpc-party/keygen/participate')
.set('Authorization', `Bearer ${authToken}`)
.send({})
.expect(400);
expect(response.body).toHaveProperty('message');
});
it('should handle internal server errors gracefully', async () => {
prismaService.partyShare.findMany.mockRejectedValue(
new Error('Database connection lost'),
);
const response = await request(app.getHttpServer())
.get('/api/v1/mpc-party/shares')
.set('Authorization', `Bearer ${authToken}`)
.expect(500);
expect(response.body).toHaveProperty('message');
});
});
});
手动测试指南
1. 使用 cURL 测试
# 1. 健康检查
curl -X GET http://localhost:3006/api/v1/mpc-party/health
# 2. 获取 JWT Token(从身份服务获取或手动生成)
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# 3. 测试 Keygen 参与
curl -X POST http://localhost:3006/api/v1/mpc-party/keygen/participate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"partyId": "user123-server",
"joinToken": "join-token-abc123",
"shareType": "wallet",
"userId": "user-id-123"
}'
# 4. 列出分片
curl -X GET "http://localhost:3006/api/v1/mpc-party/shares?page=1&limit=10" \
-H "Authorization: Bearer $TOKEN"
# 5. 获取分片信息
curl -X GET http://localhost:3006/api/v1/mpc-party/shares/share_xxx \
-H "Authorization: Bearer $TOKEN"
2. 使用 Swagger UI
-
启动开发服务器:
npm run start:dev -
访问 Swagger UI:
http://localhost:3006/api/docs -
点击 "Authorize" 按钮,输入 JWT Token
-
测试各个端点
3. 使用 Postman
-
导入 API 集合(如果有提供)
-
设置环境变量:
BASE_URL:http://localhost:3006/api/v1TOKEN: JWT Token
-
按顺序执行请求:
- 健康检查
- Keygen 参与
- 列出分片
- 获取分片详情
测试覆盖率
运行覆盖率报告
npm run test:cov
覆盖率目标
| 指标 | 目标 | 当前 |
|---|---|---|
| 语句覆盖率 | > 80% | - |
| 分支覆盖率 | > 70% | - |
| 函数覆盖率 | > 80% | - |
| 行覆盖率 | > 80% | - |
覆盖率报告输出
报告生成在 coverage/ 目录下,包含:
coverage/lcov-report/index.html- HTML 报告coverage/lcov.info- LCOV 格式报告
CI/CD 集成
GitHub Actions 示例
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate Prisma Client
run: npx prisma generate
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Run e2e tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
最佳实践
1. 测试命名规范
describe('ComponentName', () => {
describe('methodName', () => {
it('should [expected behavior] when [condition]', () => {
// ...
});
});
});
2. AAA 模式
it('should save share successfully', async () => {
// Arrange - 准备测试数据
const share = createMockShare();
prismaService.partyShare.create.mockResolvedValue(share);
// Act - 执行被测方法
await repository.save(share);
// Assert - 验证结果
expect(prismaService.partyShare.create).toHaveBeenCalled();
});
3. 模拟最佳实践
- 只模拟外部依赖,不模拟被测代码
- 使用
jest.fn()创建模拟函数 - 使用
mockResolvedValue和mockRejectedValue处理异步 - 在
beforeEach中重置模拟
4. 避免的反模式
- 测试实现细节而非行为
- 过度模拟
- 测试间相互依赖
- 测试中使用硬编码的时间等待
故障排除
常见问题
-
测试超时
- 增加 Jest 超时时间
- 检查异步操作是否正确等待
-
模拟不生效
- 确保模拟在测试之前设置
- 检查依赖注入的 token 是否正确
-
类型错误
- 确保 TypeScript 配置正确
- 检查
tsconfig.json中的paths映射
-
环境变量问题
- 确保
tests/setup.ts正确设置环境变量 - 检查
jest.config.js中的setupFilesAfterEnv
- 确保