809 lines
22 KiB
Markdown
809 lines
22 KiB
Markdown
# Reward Service 测试指南
|
||
|
||
## 测试概述
|
||
|
||
本服务采用分层测试策略,确保代码质量和业务逻辑正确性:
|
||
|
||
| 测试类型 | 目的 | 测试范围 | 依赖 |
|
||
|---------|------|---------|------|
|
||
| 单元测试 | 测试领域逻辑 | 值对象、聚合根 | 无外部依赖 |
|
||
| 集成测试 | 测试服务层 | 应用服务、领域服务 | Mock依赖 |
|
||
| E2E测试 | 测试完整流程 | API端点 | Mock服务 |
|
||
|
||
### 技术栈
|
||
|
||
- **测试框架**: Jest 30.x
|
||
- **HTTP测试**: Supertest 7.x
|
||
- **Mock工具**: Jest内置Mock
|
||
- **容器化**: Docker Compose
|
||
|
||
---
|
||
|
||
## 测试架构
|
||
|
||
```
|
||
test/
|
||
├── integration/ # 集成测试
|
||
│ ├── reward-application.service.spec.ts
|
||
│ └── reward-calculation.service.spec.ts
|
||
│
|
||
├── app.e2e-spec.ts # E2E测试
|
||
├── jest-e2e.json # E2E测试配置
|
||
└── setup.ts # 测试环境设置
|
||
|
||
src/
|
||
├── domain/
|
||
│ ├── aggregates/
|
||
│ │ ├── reward-ledger-entry/
|
||
│ │ │ └── reward-ledger-entry.spec.ts # 单元测试
|
||
│ │ └── reward-summary/
|
||
│ │ └── reward-summary.spec.ts # 单元测试
|
||
│ └── value-objects/
|
||
│ ├── money.spec.ts # 单元测试
|
||
│ └── hashpower.spec.ts # 单元测试
|
||
```
|
||
|
||
---
|
||
|
||
## 运行测试
|
||
|
||
### 快速命令
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
npm test
|
||
|
||
# 运行单元测试
|
||
make test-unit
|
||
|
||
# 运行集成测试
|
||
make test-integration
|
||
|
||
# 运行E2E测试
|
||
make test-e2e
|
||
|
||
# 运行所有测试 (Docker环境)
|
||
make test-docker-all
|
||
|
||
# 测试覆盖率
|
||
npm run test:cov
|
||
```
|
||
|
||
### Makefile 命令详解
|
||
|
||
```makefile
|
||
# 单元测试 - 测试领域逻辑和值对象
|
||
test-unit:
|
||
npm test -- --testPathPatterns='src/.*\.spec\.ts$' --verbose
|
||
|
||
# 集成测试 - 测试服务层和仓储
|
||
test-integration:
|
||
npm test -- --testPathPatterns='test/integration/.*\.spec\.ts$' --verbose
|
||
|
||
# 端到端测试 - 测试完整API流程
|
||
test-e2e:
|
||
npm run test:e2e -- --verbose
|
||
|
||
# Docker环境中运行所有测试
|
||
test-docker-all:
|
||
docker-compose -f docker-compose.test.yml up -d
|
||
sleep 5
|
||
npm test -- --testPathPatterns='src/.*\.spec\.ts$' --verbose || true
|
||
npm test -- --testPathPatterns='test/integration/.*\.spec\.ts$' --verbose || true
|
||
npm run test:e2e -- --verbose || true
|
||
docker-compose -f docker-compose.test.yml down -v
|
||
```
|
||
|
||
---
|
||
|
||
## 单元测试
|
||
|
||
单元测试针对领域层的纯业务逻辑,不依赖外部服务。
|
||
|
||
### 测试值对象
|
||
|
||
```typescript
|
||
// src/domain/value-objects/money.spec.ts
|
||
describe('Money', () => {
|
||
describe('USDT factory', () => {
|
||
it('should create Money with USDT currency', () => {
|
||
const money = Money.USDT(100);
|
||
expect(money.amount).toBe(100);
|
||
expect(money.currency).toBe('USDT');
|
||
});
|
||
});
|
||
|
||
describe('validation', () => {
|
||
it('should throw error for negative amount', () => {
|
||
expect(() => Money.USDT(-100)).toThrow('金额不能为负数');
|
||
});
|
||
});
|
||
|
||
describe('add', () => {
|
||
it('should add two Money values', () => {
|
||
const money1 = Money.USDT(100);
|
||
const money2 = Money.USDT(50);
|
||
const result = money1.add(money2);
|
||
expect(result.amount).toBe(150);
|
||
});
|
||
});
|
||
|
||
describe('subtract', () => {
|
||
it('should return zero when subtracting larger value', () => {
|
||
const money1 = Money.USDT(50);
|
||
const money2 = Money.USDT(100);
|
||
const result = money1.subtract(money2);
|
||
expect(result.amount).toBe(0);
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### 测试聚合根
|
||
|
||
```typescript
|
||
// src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts
|
||
describe('RewardLedgerEntry', () => {
|
||
describe('createPending', () => {
|
||
it('should create a pending reward with 24h expiration', () => {
|
||
const entry = RewardLedgerEntry.createPending({
|
||
userId: BigInt(100),
|
||
rewardSource: createTestRewardSource(),
|
||
usdtAmount: Money.USDT(500),
|
||
hashpowerAmount: Hashpower.zero(),
|
||
memo: 'Test reward',
|
||
});
|
||
|
||
expect(entry.isPending).toBe(true);
|
||
expect(entry.expireAt).toBeDefined();
|
||
expect(entry.getRemainingTimeMs()).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('claim', () => {
|
||
it('should transition pending to settleable', () => {
|
||
const entry = createPendingEntry();
|
||
entry.claim();
|
||
|
||
expect(entry.isSettleable).toBe(true);
|
||
expect(entry.claimedAt).toBeDefined();
|
||
expect(entry.expireAt).toBeNull();
|
||
});
|
||
|
||
it('should throw error when not pending', () => {
|
||
const entry = createSettleableEntry();
|
||
expect(() => entry.claim()).toThrow('只有待领取状态才能领取');
|
||
});
|
||
});
|
||
|
||
describe('expire', () => {
|
||
it('should transition pending to expired', () => {
|
||
const entry = createPendingEntry();
|
||
entry.expire();
|
||
|
||
expect(entry.isExpired).toBe(true);
|
||
expect(entry.expiredAt).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('settle', () => {
|
||
it('should transition settleable to settled', () => {
|
||
const entry = createSettleableEntry();
|
||
entry.settle('BNB', 0.25);
|
||
|
||
expect(entry.isSettled).toBe(true);
|
||
expect(entry.settledAt).toBeDefined();
|
||
});
|
||
|
||
it('should throw error when not settleable', () => {
|
||
const entry = createPendingEntry();
|
||
expect(() => entry.settle('BNB', 0.25)).toThrow('只有可结算状态才能结算');
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 集成测试
|
||
|
||
集成测试验证应用服务层与领域服务的协作,使用Mock隔离外部依赖。
|
||
|
||
### 测试应用服务
|
||
|
||
```typescript
|
||
// test/integration/reward-application.service.spec.ts
|
||
describe('RewardApplicationService (Integration)', () => {
|
||
let service: RewardApplicationService;
|
||
let mockLedgerRepository: jest.Mocked<IRewardLedgerEntryRepository>;
|
||
let mockSummaryRepository: jest.Mocked<IRewardSummaryRepository>;
|
||
let mockEventPublisher: jest.Mocked<EventPublisherService>;
|
||
let mockWalletService: jest.Mocked<WalletServiceClient>;
|
||
|
||
beforeEach(async () => {
|
||
// 创建Mock对象
|
||
mockLedgerRepository = {
|
||
save: jest.fn(),
|
||
saveAll: jest.fn(),
|
||
findByUserId: jest.fn(),
|
||
findPendingByUserId: jest.fn(),
|
||
findSettleableByUserId: jest.fn(),
|
||
findExpiredPending: jest.fn(),
|
||
countByUserId: jest.fn(),
|
||
};
|
||
|
||
mockSummaryRepository = {
|
||
findByUserId: jest.fn(),
|
||
getOrCreate: jest.fn(),
|
||
save: jest.fn(),
|
||
};
|
||
|
||
mockEventPublisher = {
|
||
publish: jest.fn(),
|
||
publishAll: jest.fn(),
|
||
};
|
||
|
||
mockWalletService = {
|
||
executeSwap: jest.fn(),
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
RewardApplicationService,
|
||
RewardCalculationService,
|
||
RewardExpirationService,
|
||
{
|
||
provide: REWARD_LEDGER_ENTRY_REPOSITORY,
|
||
useValue: mockLedgerRepository,
|
||
},
|
||
{
|
||
provide: REWARD_SUMMARY_REPOSITORY,
|
||
useValue: mockSummaryRepository,
|
||
},
|
||
{
|
||
provide: EventPublisherService,
|
||
useValue: mockEventPublisher,
|
||
},
|
||
{
|
||
provide: WalletServiceClient,
|
||
useValue: mockWalletService,
|
||
},
|
||
// ... 其他Mock
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<RewardApplicationService>(RewardApplicationService);
|
||
});
|
||
|
||
describe('distributeRewards', () => {
|
||
it('should distribute rewards and update summaries', async () => {
|
||
// Arrange
|
||
const params = {
|
||
sourceOrderId: BigInt(1),
|
||
sourceUserId: BigInt(100),
|
||
treeCount: 10,
|
||
provinceCode: '440000',
|
||
cityCode: '440100',
|
||
};
|
||
|
||
mockSummaryRepository.getOrCreate.mockResolvedValue(
|
||
RewardSummary.create(BigInt(100))
|
||
);
|
||
|
||
// Act
|
||
await service.distributeRewards(params);
|
||
|
||
// Assert
|
||
expect(mockLedgerRepository.saveAll).toHaveBeenCalled();
|
||
expect(mockSummaryRepository.save).toHaveBeenCalled();
|
||
expect(mockEventPublisher.publishAll).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('settleRewards', () => {
|
||
it('should settle rewards and call wallet service', async () => {
|
||
// Arrange
|
||
const settleableRewards = [createSettleableEntry()];
|
||
mockLedgerRepository.findSettleableByUserId.mockResolvedValue(settleableRewards);
|
||
mockSummaryRepository.getOrCreate.mockResolvedValue(
|
||
RewardSummary.create(BigInt(100))
|
||
);
|
||
mockWalletService.executeSwap.mockResolvedValue({
|
||
success: true,
|
||
receivedAmount: 0.25,
|
||
txHash: '0x123',
|
||
});
|
||
|
||
// Act
|
||
const result = await service.settleRewards({
|
||
userId: BigInt(100),
|
||
settleCurrency: 'BNB',
|
||
});
|
||
|
||
// Assert
|
||
expect(result.success).toBe(true);
|
||
expect(result.receivedAmount).toBe(0.25);
|
||
expect(mockWalletService.executeSwap).toHaveBeenCalledWith({
|
||
userId: BigInt(100),
|
||
usdtAmount: expect.any(Number),
|
||
targetCurrency: 'BNB',
|
||
});
|
||
});
|
||
|
||
it('should return error when no settleable rewards', async () => {
|
||
// Arrange
|
||
mockLedgerRepository.findSettleableByUserId.mockResolvedValue([]);
|
||
|
||
// Act
|
||
const result = await service.settleRewards({
|
||
userId: BigInt(100),
|
||
settleCurrency: 'BNB',
|
||
});
|
||
|
||
// Assert
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('没有可结算的收益');
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### 测试领域服务
|
||
|
||
```typescript
|
||
// test/integration/reward-calculation.service.spec.ts
|
||
describe('RewardCalculationService (Integration)', () => {
|
||
let service: RewardCalculationService;
|
||
let mockReferralService: jest.Mocked<IReferralServiceClient>;
|
||
let mockAuthorizationService: jest.Mocked<IAuthorizationServiceClient>;
|
||
|
||
beforeEach(async () => {
|
||
mockReferralService = {
|
||
getReferralChain: jest.fn(),
|
||
};
|
||
|
||
mockAuthorizationService = {
|
||
findNearestAuthorizedProvince: jest.fn(),
|
||
findNearestAuthorizedCity: jest.fn(),
|
||
findNearestCommunity: jest.fn(),
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
RewardCalculationService,
|
||
{
|
||
provide: REFERRAL_SERVICE_CLIENT,
|
||
useValue: mockReferralService,
|
||
},
|
||
{
|
||
provide: AUTHORIZATION_SERVICE_CLIENT,
|
||
useValue: mockAuthorizationService,
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<RewardCalculationService>(RewardCalculationService);
|
||
});
|
||
|
||
describe('calculateRewards', () => {
|
||
const baseParams = {
|
||
sourceOrderId: BigInt(1),
|
||
sourceUserId: BigInt(100),
|
||
treeCount: 10,
|
||
provinceCode: '440000',
|
||
cityCode: '440100',
|
||
};
|
||
|
||
it('should calculate all 6 types of rewards', async () => {
|
||
// Arrange
|
||
mockReferralService.getReferralChain.mockResolvedValue({
|
||
ancestors: [{ userId: BigInt(200), hasPlanted: true }],
|
||
});
|
||
mockAuthorizationService.findNearestAuthorizedProvince.mockResolvedValue(BigInt(300));
|
||
mockAuthorizationService.findNearestAuthorizedCity.mockResolvedValue(BigInt(400));
|
||
mockAuthorizationService.findNearestCommunity.mockResolvedValue(BigInt(500));
|
||
|
||
// Act
|
||
const rewards = await service.calculateRewards(baseParams);
|
||
|
||
// Assert
|
||
expect(rewards).toHaveLength(6);
|
||
|
||
const rightTypes = rewards.map(r => r.rewardSource.rightType);
|
||
expect(rightTypes).toContain(RightType.SHARE_RIGHT);
|
||
expect(rightTypes).toContain(RightType.PROVINCE_TEAM_RIGHT);
|
||
expect(rightTypes).toContain(RightType.PROVINCE_AREA_RIGHT);
|
||
expect(rightTypes).toContain(RightType.CITY_TEAM_RIGHT);
|
||
expect(rightTypes).toContain(RightType.CITY_AREA_RIGHT);
|
||
expect(rightTypes).toContain(RightType.COMMUNITY_RIGHT);
|
||
});
|
||
|
||
it('should calculate share right reward (500 USDT) when referrer has planted', async () => {
|
||
// Arrange
|
||
mockReferralService.getReferralChain.mockResolvedValue({
|
||
ancestors: [{ userId: BigInt(200), hasPlanted: true }],
|
||
});
|
||
// ... 其他Mock设置
|
||
|
||
// Act
|
||
const rewards = await service.calculateRewards(baseParams);
|
||
|
||
// Assert
|
||
const shareReward = rewards.find(
|
||
r => r.rewardSource.rightType === RightType.SHARE_RIGHT
|
||
);
|
||
expect(shareReward).toBeDefined();
|
||
expect(shareReward?.isSettleable).toBe(true);
|
||
expect(shareReward?.usdtAmount.amount).toBe(500 * 10);
|
||
});
|
||
|
||
it('should create pending share right reward when referrer has not planted', async () => {
|
||
// Arrange
|
||
mockReferralService.getReferralChain.mockResolvedValue({
|
||
ancestors: [{ userId: BigInt(200), hasPlanted: false }],
|
||
});
|
||
|
||
// Act
|
||
const rewards = await service.calculateRewards(baseParams);
|
||
|
||
// Assert
|
||
const shareReward = rewards.find(
|
||
r => r.rewardSource.rightType === RightType.SHARE_RIGHT
|
||
);
|
||
expect(shareReward?.isPending).toBe(true);
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## E2E测试
|
||
|
||
E2E测试验证完整的HTTP请求-响应流程。
|
||
|
||
### 测试配置
|
||
|
||
```json
|
||
// test/jest-e2e.json
|
||
{
|
||
"moduleFileExtensions": ["js", "json", "ts"],
|
||
"rootDir": ".",
|
||
"testEnvironment": "node",
|
||
"testRegex": ".e2e-spec.ts$",
|
||
"transform": {
|
||
"^.+\\.(t|j)s$": "ts-jest"
|
||
},
|
||
"moduleNameMapper": {
|
||
"^src/(.*)$": "<rootDir>/../src/$1"
|
||
}
|
||
}
|
||
```
|
||
|
||
### E2E测试示例
|
||
|
||
```typescript
|
||
// test/app.e2e-spec.ts
|
||
describe('Reward Service (e2e)', () => {
|
||
let app: INestApplication;
|
||
let jwtService: JwtService;
|
||
let mockRewardService: any;
|
||
|
||
const TEST_JWT_SECRET = 'test-secret-key-for-testing';
|
||
|
||
const createTestToken = (userId: string = '100') => {
|
||
return jwtService.sign({
|
||
sub: userId,
|
||
username: 'testuser',
|
||
roles: ['user'],
|
||
});
|
||
};
|
||
|
||
beforeEach(async () => {
|
||
mockRewardService = {
|
||
getRewardSummary: jest.fn().mockResolvedValue({
|
||
pendingUsdt: 1000,
|
||
pendingHashpower: 0.5,
|
||
pendingExpireAt: new Date(Date.now() + 12 * 60 * 60 * 1000),
|
||
settleableUsdt: 500,
|
||
settleableHashpower: 0.2,
|
||
settledTotalUsdt: 2000,
|
||
settledTotalHashpower: 1.0,
|
||
expiredTotalUsdt: 100,
|
||
expiredTotalHashpower: 0.1,
|
||
}),
|
||
// ... 其他Mock方法
|
||
};
|
||
|
||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||
imports: [
|
||
ConfigModule.forRoot({
|
||
isGlobal: true,
|
||
load: [() => ({ JWT_SECRET: TEST_JWT_SECRET })],
|
||
}),
|
||
PassportModule,
|
||
JwtModule.register({
|
||
secret: TEST_JWT_SECRET,
|
||
signOptions: { expiresIn: '1h' },
|
||
}),
|
||
],
|
||
controllers: [HealthController, RewardController, SettlementController],
|
||
providers: [
|
||
{
|
||
provide: RewardApplicationService,
|
||
useValue: mockRewardService,
|
||
},
|
||
// ... JwtStrategy配置
|
||
],
|
||
}).compile();
|
||
|
||
app = moduleFixture.createNestApplication();
|
||
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||
await app.init();
|
||
|
||
jwtService = moduleFixture.get<JwtService>(JwtService);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await app.close();
|
||
});
|
||
|
||
describe('Health Check', () => {
|
||
it('/health (GET) should return healthy status', () => {
|
||
return request(app.getHttpServer())
|
||
.get('/health')
|
||
.expect(200)
|
||
.expect((res) => {
|
||
expect(res.body.status).toBe('ok');
|
||
expect(res.body.service).toBe('reward-service');
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('Rewards API', () => {
|
||
describe('GET /rewards/summary', () => {
|
||
it('should return 401 without auth token', () => {
|
||
return request(app.getHttpServer())
|
||
.get('/rewards/summary')
|
||
.expect(401);
|
||
});
|
||
|
||
it('should return reward summary with valid token', () => {
|
||
const token = createTestToken();
|
||
return request(app.getHttpServer())
|
||
.get('/rewards/summary')
|
||
.set('Authorization', `Bearer ${token}`)
|
||
.expect(200)
|
||
.expect((res) => {
|
||
expect(res.body.pendingUsdt).toBe(1000);
|
||
expect(res.body.settleableUsdt).toBe(500);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('Settlement API', () => {
|
||
describe('POST /rewards/settle', () => {
|
||
it('should settle rewards successfully with valid token', () => {
|
||
const token = createTestToken();
|
||
return request(app.getHttpServer())
|
||
.post('/rewards/settle')
|
||
.set('Authorization', `Bearer ${token}`)
|
||
.send({ settleCurrency: 'BNB' })
|
||
.expect(201)
|
||
.expect((res) => {
|
||
expect(res.body.success).toBe(true);
|
||
});
|
||
});
|
||
|
||
it('should validate settleCurrency parameter', () => {
|
||
const token = createTestToken();
|
||
return request(app.getHttpServer())
|
||
.post('/rewards/settle')
|
||
.set('Authorization', `Bearer ${token}`)
|
||
.send({ settleCurrency: '' })
|
||
.expect(400);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Docker测试环境
|
||
|
||
### docker-compose.test.yml
|
||
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:15-alpine
|
||
container_name: reward-test-postgres
|
||
environment:
|
||
POSTGRES_USER: test
|
||
POSTGRES_PASSWORD: test
|
||
POSTGRES_DB: reward_test
|
||
ports:
|
||
- '5433:5432'
|
||
healthcheck:
|
||
test: ['CMD-SHELL', 'pg_isready -U test -d reward_test']
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
container_name: reward-test-redis
|
||
ports:
|
||
- '6380:6379'
|
||
healthcheck:
|
||
test: ['CMD', 'redis-cli', 'ping']
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
kafka:
|
||
image: confluentinc/cp-kafka:7.5.0
|
||
container_name: reward-test-kafka
|
||
depends_on:
|
||
- zookeeper
|
||
ports:
|
||
- '9093:9092'
|
||
environment:
|
||
KAFKA_BROKER_ID: 1
|
||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||
# ... 其他配置
|
||
```
|
||
|
||
### 运行Docker测试
|
||
|
||
```bash
|
||
# 启动测试依赖
|
||
make docker-up
|
||
|
||
# 运行测试
|
||
make test-docker-all
|
||
|
||
# 关闭测试依赖
|
||
make docker-down
|
||
```
|
||
|
||
---
|
||
|
||
## 测试覆盖率
|
||
|
||
### 生成覆盖率报告
|
||
|
||
```bash
|
||
npm run test:cov
|
||
```
|
||
|
||
### 覆盖率目标
|
||
|
||
| 指标 | 目标 |
|
||
|------|------|
|
||
| 语句覆盖率 (Statements) | >= 80% |
|
||
| 分支覆盖率 (Branches) | >= 75% |
|
||
| 函数覆盖率 (Functions) | >= 85% |
|
||
| 行覆盖率 (Lines) | >= 80% |
|
||
|
||
### 查看报告
|
||
|
||
覆盖率报告生成在 `coverage/` 目录:
|
||
|
||
```bash
|
||
# 在浏览器中打开HTML报告
|
||
open coverage/lcov-report/index.html
|
||
```
|
||
|
||
---
|
||
|
||
## 测试最佳实践
|
||
|
||
### 1. 测试命名规范
|
||
|
||
```typescript
|
||
// 使用 describe 组织测试
|
||
describe('RewardLedgerEntry', () => {
|
||
describe('claim', () => {
|
||
it('should transition pending to settleable', () => { ... });
|
||
it('should throw error when not pending', () => { ... });
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2. AAA 模式
|
||
|
||
```typescript
|
||
it('should calculate total amount', () => {
|
||
// Arrange - 准备测试数据
|
||
const reward1 = createReward(100);
|
||
const reward2 = createReward(200);
|
||
|
||
// Act - 执行被测试的行为
|
||
const total = service.calculateTotal([reward1, reward2]);
|
||
|
||
// Assert - 验证结果
|
||
expect(total).toBe(300);
|
||
});
|
||
```
|
||
|
||
### 3. 使用工厂函数创建测试数据
|
||
|
||
```typescript
|
||
// test/helpers/factories.ts
|
||
export function createTestRewardSource(overrides = {}) {
|
||
return RewardSource.create(
|
||
RightType.SHARE_RIGHT,
|
||
BigInt(1),
|
||
BigInt(100),
|
||
...overrides,
|
||
);
|
||
}
|
||
|
||
export function createPendingEntry(overrides = {}) {
|
||
return RewardLedgerEntry.createPending({
|
||
userId: BigInt(100),
|
||
rewardSource: createTestRewardSource(),
|
||
usdtAmount: Money.USDT(500),
|
||
hashpowerAmount: Hashpower.zero(),
|
||
...overrides,
|
||
});
|
||
}
|
||
```
|
||
|
||
### 4. 避免测试实现细节
|
||
|
||
```typescript
|
||
// ❌ 测试实现细节
|
||
expect(service['privateField']).toBe(expectedValue);
|
||
|
||
// ✅ 测试行为
|
||
const result = await service.publicMethod();
|
||
expect(result).toMatchObject({ status: 'success' });
|
||
```
|
||
|
||
### 5. 使用 Mock 隔离依赖
|
||
|
||
```typescript
|
||
// 创建类型安全的Mock
|
||
const mockRepository = {
|
||
save: jest.fn(),
|
||
findById: jest.fn(),
|
||
} as jest.Mocked<IRewardLedgerEntryRepository>;
|
||
|
||
// 设置Mock返回值
|
||
mockRepository.findById.mockResolvedValue(expectedEntry);
|
||
|
||
// 验证Mock被调用
|
||
expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
||
userId: BigInt(100),
|
||
}));
|
||
```
|
||
|
||
---
|
||
|
||
## 测试报告示例
|
||
|
||
```
|
||
Test Suites: 7 passed, 7 total
|
||
Tests: 77 passed, 77 total
|
||
Snapshots: 0 total
|
||
Time: 13.026 s
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 测试结果汇总 │
|
||
├─────────────────┬──────────┬──────────┬──────────┬─────────┤
|
||
│ 测试类型 │ 测试套件 │ 测试用例 │ 状态 │ 耗时 │
|
||
├─────────────────┼──────────┼──────────┼──────────┼─────────┤
|
||
│ 单元测试 │ 4 │ 43 │ ✅ 通过 │ 3.2s │
|
||
│ 集成测试 │ 2 │ 20 │ ✅ 通过 │ 4.8s │
|
||
│ E2E测试 │ 1 │ 14 │ ✅ 通过 │ 5.0s │
|
||
├─────────────────┼──────────┼──────────┼──────────┼─────────┤
|
||
│ 总计 │ 7 │ 77 │ ✅ 通过 │ ~13s │
|
||
└─────────────────┴──────────┴──────────┴──────────┴─────────┘
|
||
```
|