# MPC Party Service 测试文档 ## 概述 本文档详细说明 MPC Party Service 的测试架构、测试策略和实现方式。 ## 测试金字塔 ``` ┌─────────┐ │ E2E │ ← 端到端测试 (15 tests) ┌┴─────────┴┐ │Integration │ ← 集成测试 (30 tests) ┌┴───────────┴┐ │ Unit Tests │ ← 单元测试 (81 tests) └───────────────┘ 总计: 111+ 测试用例 ``` ## 测试分类 ### 1. 单元测试 (Unit Tests) 测试单个组件的隔离行为,不依赖外部服务。 **目录**: `tests/unit/` **运行命令**: ```bash npm run test:unit ``` **覆盖范围**: - 领域实体 (Domain Entities) - 值对象 (Value Objects) - 领域服务 (Domain Services) - 应用层处理器 (Application Handlers) - 数据映射器 (Data Mappers) ### 2. 集成测试 (Integration Tests) 测试多个组件之间的交互,使用模拟的外部依赖。 **目录**: `tests/integration/` **运行命令**: ```bash npm run test:integration ``` **覆盖范围**: - 仓储实现 (Repository Implementations) - 控制器 (Controllers) - 事件发布器 (Event Publishers) ### 3. 端到端测试 (E2E Tests) 测试完整的 API 流程,模拟真实的客户端请求。 **目录**: `tests/e2e/` **运行命令**: ```bash npm run test:e2e ``` **覆盖范围**: - API 端点 - 认证流程 - 错误处理 - 完整的请求/响应周期 --- ## 测试配置 ### Jest 配置文件 #### 基础配置 (`tests/jest.config.js`) ```javascript module.exports = { moduleFileExtensions: ['js', 'json', 'ts'], rootDir: '..', testEnvironment: 'node', transform: { '^.+\\.(t|j)s$': 'ts-jest', }, moduleNameMapper: { '^@/(.*)$': '/src/$1', }, setupFilesAfterEnv: ['/tests/setup.ts'], collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.module.ts', '!src/main.ts', ], coverageDirectory: './coverage', testMatch: [ '/tests/unit/**/*.spec.ts', '/tests/integration/**/*.spec.ts', ], }; ``` #### 单元测试配置 (`tests/jest-unit.config.js`) ```javascript const baseConfig = require('./jest.config'); module.exports = { ...baseConfig, testMatch: ['/tests/unit/**/*.spec.ts'], testTimeout: 30000, }; ``` #### 集成测试配置 (`tests/jest-integration.config.js`) ```javascript const baseConfig = require('./jest.config'); module.exports = { ...baseConfig, testMatch: ['/tests/integration/**/*.spec.ts'], testTimeout: 60000, }; ``` #### E2E 测试配置 (`tests/jest-e2e.config.js`) ```javascript const baseConfig = require('./jest.config'); module.exports = { ...baseConfig, testMatch: ['/tests/e2e/**/*.e2e-spec.ts'], testTimeout: 120000, maxWorkers: 1, // 顺序执行 }; ``` ### 测试环境设置 (`tests/setup.ts`) ```typescript 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`) ```typescript 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`) ```typescript 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`) ```typescript 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`) ```typescript 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); }); 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`) ```typescript 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); }); 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`) ```typescript 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); }); 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`) ```typescript 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); // 生成测试令牌 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 测试 ```bash # 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 1. 启动开发服务器: ```bash npm run start:dev ``` 2. 访问 Swagger UI: ``` http://localhost:3006/api/docs ``` 3. 点击 "Authorize" 按钮,输入 JWT Token 4. 测试各个端点 ### 3. 使用 Postman 1. 导入 API 集合(如果有提供) 2. 设置环境变量: - `BASE_URL`: `http://localhost:3006/api/v1` - `TOKEN`: JWT Token 3. 按顺序执行请求: - 健康检查 - Keygen 参与 - 列出分片 - 获取分片详情 --- ## 测试覆盖率 ### 运行覆盖率报告 ```bash npm run test:cov ``` ### 覆盖率目标 | 指标 | 目标 | 当前 | |------|------|------| | 语句覆盖率 | > 80% | - | | 分支覆盖率 | > 70% | - | | 函数覆盖率 | > 80% | - | | 行覆盖率 | > 80% | - | ### 覆盖率报告输出 报告生成在 `coverage/` 目录下,包含: - `coverage/lcov-report/index.html` - HTML 报告 - `coverage/lcov.info` - LCOV 格式报告 --- ## CI/CD 集成 ### GitHub Actions 示例 ```yaml # .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. 测试命名规范 ```typescript describe('ComponentName', () => { describe('methodName', () => { it('should [expected behavior] when [condition]', () => { // ... }); }); }); ``` ### 2. AAA 模式 ```typescript 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. 避免的反模式 - 测试实现细节而非行为 - 过度模拟 - 测试间相互依赖 - 测试中使用硬编码的时间等待 --- ## 故障排除 ### 常见问题 1. **测试超时** - 增加 Jest 超时时间 - 检查异步操作是否正确等待 2. **模拟不生效** - 确保模拟在测试之前设置 - 检查依赖注入的 token 是否正确 3. **类型错误** - 确保 TypeScript 配置正确 - 检查 `tsconfig.json` 中的 `paths` 映射 4. **环境变量问题** - 确保 `tests/setup.ts` 正确设置环境变量 - 检查 `jest.config.js` 中的 `setupFilesAfterEnv`