# Leaderboard Service 测试文档

## 1. 测试架构概述

本服务采用分层测试策略，包含单元测试、集成测试和端到端测试（E2E），确保代码质量和系统可靠性。

### 1.1 测试金字塔

```
                    ┌─────────┐
                    │   E2E   │  少量 - 关键用户流程
                    │  Tests  │
                    └────┬────┘
                         │
                ┌────────┴────────┐
                │   Integration   │  中等 - 组件集成
                │     Tests       │
                └────────┬────────┘
                         │
        ┌────────────────┴────────────────┐
        │          Unit Tests              │  大量 - 业务逻辑
        │  (Domain, Services, Utilities)   │
        └──────────────────────────────────┘
```

### 1.2 测试技术栈

| 工具 | 用途 |
|------|------|
| Jest | 测试框架 |
| ts-jest | TypeScript 支持 |
| @nestjs/testing | NestJS 测试工具 |
| supertest | HTTP 请求测试 |
| Docker Compose | 测试环境容器化 |

### 1.3 测试目录结构

```
test/
├── domain/                          # 领域层单元测试
│   ├── value-objects/
│   │   ├── rank-position.vo.spec.ts
│   │   ├── ranking-score.vo.spec.ts
│   │   └── leaderboard-period.vo.spec.ts
│   ├── aggregates/
│   │   └── leaderboard-config.aggregate.spec.ts
│   └── services/
│       └── ranking-merger.service.spec.ts
│
├── integration/                     # 集成测试
│   └── leaderboard-repository.integration.spec.ts
│
├── app.e2e-spec.ts                 # E2E 测试
├── setup-integration.ts            # 集成测试设置
├── setup-e2e.ts                    # E2E 测试设置
├── jest-integration.json           # 集成测试配置
└── jest-e2e.json                   # E2E 测试配置
```

## 2. 单元测试

### 2.1 运行单元测试

```bash
# 运行所有单元测试
npm test

# 监听模式
npm run test:watch

# 生成覆盖率报告
npm run test:cov

# 调试模式
npm run test:debug
```

### 2.2 测试覆盖率目标

| 层级 | 目标覆盖率 |
|------|-----------|
| Domain (Value Objects) | >= 90% |
| Domain (Aggregates) | >= 85% |
| Domain (Services) | >= 85% |
| Application Services | >= 80% |
| Infrastructure | >= 70% |

### 2.3 值对象测试示例

```typescript
// test/domain/value-objects/ranking-score.vo.spec.ts
import { RankingScore } from '../../../src/domain/value-objects/ranking-score.vo';

describe('RankingScore', () => {
  describe('calculate', () => {
    it('应该正确计算龙虎榜分值', () => {
      // 用户团队数据：
      // - 团队总认种: 230棵
      // - 最大单个直推团队: 100棵
      // - 龙虎榜分值: 230 - 100 = 130
      const score = RankingScore.calculate(230, 100);

      expect(score.totalTeamPlanting).toBe(230);
      expect(score.maxDirectTeamPlanting).toBe(100);
      expect(score.effectiveScore).toBe(130);
    });

    it('当团队总认种等于最大直推时，有效分值为0', () => {
      const score = RankingScore.calculate(100, 100);
      expect(score.effectiveScore).toBe(0);
    });

    it('有效分值不能为负数', () => {
      const score = RankingScore.calculate(50, 100);
      expect(score.effectiveScore).toBe(0);
    });
  });

  describe('compareTo', () => {
    it('分值高的应该排在前面', () => {
      const score1 = RankingScore.calculate(200, 50); // 有效分值: 150
      const score2 = RankingScore.calculate(150, 50); // 有效分值: 100

      expect(score1.compareTo(score2)).toBeLessThan(0); // score1 排名更靠前
    });
  });

  describe('isHealthyTeamStructure', () => {
    it('大腿占比低于50%应该是健康结构', () => {
      const score = RankingScore.calculate(300, 100); // 33.3%
      expect(score.isHealthyTeamStructure()).toBe(true);
    });

    it('大腿占比高于50%应该不是健康结构', () => {
      const score = RankingScore.calculate(200, 150); // 75%
      expect(score.isHealthyTeamStructure()).toBe(false);
    });
  });
});
```

### 2.4 聚合根测试示例

```typescript
// test/domain/aggregates/leaderboard-config.aggregate.spec.ts
import { LeaderboardConfig } from '../../../src/domain/aggregates/leaderboard-config/leaderboard-config.aggregate';
import { LeaderboardType } from '../../../src/domain/value-objects/leaderboard-type.enum';

describe('LeaderboardConfig', () => {
  describe('createDefault', () => {
    it('应该创建默认配置', () => {
      const config = LeaderboardConfig.createDefault();

      expect(config.configKey).toBe('GLOBAL');
      expect(config.dailyEnabled).toBe(true);
      expect(config.weeklyEnabled).toBe(true);
      expect(config.monthlyEnabled).toBe(true);
      expect(config.virtualRankingEnabled).toBe(false);
      expect(config.virtualAccountCount).toBe(0);
      expect(config.displayLimit).toBe(30);
      expect(config.refreshIntervalMinutes).toBe(5);
    });
  });

  describe('updateLeaderboardSwitch', () => {
    it('应该更新日榜开关', () => {
      const config = LeaderboardConfig.createDefault();
      config.updateLeaderboardSwitch('daily', false, 'admin');

      expect(config.dailyEnabled).toBe(false);
      expect(config.domainEvents.length).toBe(1);
    });
  });

  describe('updateVirtualRankingSettings', () => {
    it('应该更新虚拟排名设置', () => {
      const config = LeaderboardConfig.createDefault();
      config.updateVirtualRankingSettings(true, 30, 'admin');

      expect(config.virtualRankingEnabled).toBe(true);
      expect(config.virtualAccountCount).toBe(30);
    });

    it('虚拟账户数量为负数时应该抛出错误', () => {
      const config = LeaderboardConfig.createDefault();

      expect(() => {
        config.updateVirtualRankingSettings(true, -1, 'admin');
      }).toThrow('虚拟账户数量不能为负数');
    });
  });

  describe('updateDisplayLimit', () => {
    it('显示数量为0时应该抛出错误', () => {
      const config = LeaderboardConfig.createDefault();

      expect(() => {
        config.updateDisplayLimit(0, 'admin');
      }).toThrow('显示数量必须大于0');
    });
  });
});
```

### 2.5 领域服务测试示例

```typescript
// test/domain/services/ranking-merger.service.spec.ts
import { RankingMergerService } from '../../../src/domain/services/ranking-merger.service';
import { LeaderboardRanking } from '../../../src/domain/aggregates/leaderboard-ranking/leaderboard-ranking.aggregate';

describe('RankingMergerService', () => {
  let service: RankingMergerService;

  beforeEach(() => {
    service = new RankingMergerService();
  });

  describe('mergeRankings', () => {
    it('没有虚拟排名时应该保持原始排名', () => {
      const realRankings = [
        createRealRanking(1n, 1),
        createRealRanking(2n, 2),
        createRealRanking(3n, 3),
      ];

      const merged = service.mergeRankings([], realRankings, 30);

      expect(merged.length).toBe(3);
      expect(merged[0].displayPosition.value).toBe(1);
      expect(merged[1].displayPosition.value).toBe(2);
      expect(merged[2].displayPosition.value).toBe(3);
    });

    it('有虚拟排名时应该正确调整真实用户排名', () => {
      const virtualRankings = [
        createVirtualRanking(100n, 1),
        createVirtualRanking(101n, 2),
      ];

      const realRankings = [
        createRealRanking(1n, 1),
        createRealRanking(2n, 2),
      ];

      const merged = service.mergeRankings(virtualRankings, realRankings, 30);

      expect(merged.length).toBe(4);
      expect(merged[0].isVirtual).toBe(true);
      expect(merged[2].isVirtual).toBe(false);
      expect(merged[2].displayPosition.value).toBe(3); // 原来第1名变成第3名
    });

    it('应该遵守显示数量限制', () => {
      const virtualRankings = [
        createVirtualRanking(100n, 1),
        createVirtualRanking(101n, 2),
      ];

      const realRankings = [
        createRealRanking(1n, 1),
        createRealRanking(2n, 2),
        createRealRanking(3n, 3),
      ];

      const merged = service.mergeRankings(virtualRankings, realRankings, 3);

      expect(merged.length).toBe(3);
    });
  });
});
```

## 3. 集成测试

### 3.1 运行集成测试

```bash
# 启动测试数据库
docker compose -f docker-compose.test.yml up -d postgres-test redis-test

# 推送 schema
DATABASE_URL='postgresql://postgres:postgres@localhost:5433/leaderboard_test_db' npx prisma db push

# 运行集成测试
DATABASE_URL='postgresql://postgres:postgres@localhost:5433/leaderboard_test_db' npm run test:integration

# 清理测试环境
docker compose -f docker-compose.test.yml down -v
```

### 3.2 集成测试配置

```json
// test/jest-integration.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": "..",
  "testRegex": ".*\\.integration\\.spec\\.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "collectCoverageFrom": ["src/**/*.(t|j)s", "!src/main.ts", "!src/**/*.module.ts"],
  "coverageDirectory": "./coverage/integration",
  "testEnvironment": "node",
  "moduleNameMapper": {
    "^@/(.*)$": "<rootDir>/src/$1"
  },
  "setupFilesAfterEnv": ["<rootDir>/test/setup-integration.ts"],
  "testTimeout": 30000
}
```

### 3.3 集成测试设置

```typescript
// test/setup-integration.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

beforeAll(async () => {
  try {
    await prisma.$connect();
    console.log('Database connected for integration tests');
  } catch (error) {
    console.warn('Database not available for integration tests');
  }
});

afterAll(async () => {
  await prisma.$disconnect();
});

global.testUtils = {
  prisma,
  cleanDatabase: async () => {
    const tablenames = await prisma.$queryRaw<
      Array<{ tablename: string }>
    >`SELECT tablename FROM pg_tables WHERE schemaname='public'`;

    for (const { tablename } of tablenames) {
      if (tablename !== '_prisma_migrations') {
        await prisma.$executeRawUnsafe(
          `TRUNCATE TABLE "public"."${tablename}" CASCADE;`
        );
      }
    }
  },
};
```

### 3.4 集成测试示例

```typescript
// test/integration/leaderboard-repository.integration.spec.ts
import { LeaderboardType } from '../../src/domain/value-objects/leaderboard-type.enum';
import { LeaderboardPeriod } from '../../src/domain/value-objects/leaderboard-period.vo';

describe('LeaderboardRepository Integration Tests', () => {
  describe('Database Connection', () => {
    it('should connect to the database', async () => {
      const result = await global.testUtils.prisma.$queryRaw`SELECT 1 as result`;
      expect(result).toBeDefined();
    });
  });

  describe('LeaderboardConfig Operations', () => {
    beforeEach(async () => {
      await global.testUtils.cleanDatabase();
    });

    it('should create and retrieve leaderboard config', async () => {
      const config = await global.testUtils.prisma.leaderboardConfig.create({
        data: {
          configKey: 'TEST_CONFIG',
          dailyEnabled: true,
          weeklyEnabled: true,
          monthlyEnabled: true,
          virtualRankingEnabled: false,
          virtualAccountCount: 0,
          displayLimit: 30,
          refreshIntervalMinutes: 5,
        },
      });

      expect(config.id).toBeDefined();
      expect(config.configKey).toBe('TEST_CONFIG');

      const retrieved = await global.testUtils.prisma.leaderboardConfig.findUnique({
        where: { configKey: 'TEST_CONFIG' },
      });

      expect(retrieved?.displayLimit).toBe(30);
    });
  });

  describe('LeaderboardRanking Operations', () => {
    beforeEach(async () => {
      await global.testUtils.cleanDatabase();
    });

    it('should create leaderboard ranking entries', async () => {
      const period = LeaderboardPeriod.currentDaily();

      const ranking = await global.testUtils.prisma.leaderboardRanking.create({
        data: {
          leaderboardType: LeaderboardType.DAILY,
          periodKey: period.key,
          periodStartAt: period.startAt,
          periodEndAt: period.endAt,
          userId: BigInt(1),
          rankPosition: 1,
          displayPosition: 1,
          totalTeamPlanting: 200,
          maxDirectTeamPlanting: 50,
          effectiveScore: 150,
          isVirtual: false,
          userSnapshot: { nickname: 'TestUser', avatar: null },
        },
      });

      expect(ranking.id).toBeDefined();
      expect(ranking.rankPosition).toBe(1);
      expect(ranking.effectiveScore).toBe(150);
    });

    it('should query rankings by period and type', async () => {
      const period = LeaderboardPeriod.currentDaily();

      await global.testUtils.prisma.leaderboardRanking.createMany({
        data: [
          {
            leaderboardType: LeaderboardType.DAILY,
            periodKey: period.key,
            periodStartAt: period.startAt,
            periodEndAt: period.endAt,
            userId: BigInt(1),
            rankPosition: 1,
            displayPosition: 1,
            totalTeamPlanting: 300,
            maxDirectTeamPlanting: 100,
            effectiveScore: 200,
            isVirtual: false,
            userSnapshot: { nickname: 'User1', avatar: null },
          },
          {
            leaderboardType: LeaderboardType.DAILY,
            periodKey: period.key,
            periodStartAt: period.startAt,
            periodEndAt: period.endAt,
            userId: BigInt(2),
            rankPosition: 2,
            displayPosition: 2,
            totalTeamPlanting: 200,
            maxDirectTeamPlanting: 50,
            effectiveScore: 150,
            isVirtual: false,
            userSnapshot: { nickname: 'User2', avatar: null },
          },
        ],
      });

      const rankings = await global.testUtils.prisma.leaderboardRanking.findMany({
        where: {
          leaderboardType: LeaderboardType.DAILY,
          periodKey: period.key,
        },
        orderBy: { rankPosition: 'asc' },
      });

      expect(rankings.length).toBe(2);
      expect(rankings[0].effectiveScore).toBe(200);
      expect(rankings[1].effectiveScore).toBe(150);
    });
  });
});
```

## 4. 端到端测试 (E2E)

### 4.1 运行 E2E 测试

```bash
# 启动完整测试环境
docker compose -f docker-compose.test.yml up -d

# 运行 E2E 测试
npm run test:e2e

# 清理
docker compose -f docker-compose.test.yml down -v
```

### 4.2 E2E 测试配置

```json
// test/jest-e2e.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": "..",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "coverageDirectory": "./coverage/e2e",
  "testEnvironment": "node",
  "moduleNameMapper": {
    "^@/(.*)$": "<rootDir>/src/$1"
  },
  "setupFilesAfterEnv": ["<rootDir>/test/setup-e2e.ts"],
  "testTimeout": 60000
}
```

### 4.3 E2E 测试示例

```typescript
// test/app.e2e-spec.ts
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';

describe('Leaderboard Service E2E Tests', () => {
  let app: INestApplication;

  beforeAll(() => {
    app = global.testApp;
  });

  describe('Health Check', () => {
    it('/health (GET) - should return health status', async () => {
      const response = await request(app.getHttpServer())
        .get('/health')
        .expect(200);

      expect(response.body).toHaveProperty('status');
      expect(response.body.status).toBe('ok');
    });

    it('/health/ready (GET) - should return readiness status', async () => {
      const response = await request(app.getHttpServer())
        .get('/health/ready')
        .expect(200);

      expect(response.body).toHaveProperty('status');
    });
  });

  describe('Leaderboard API', () => {
    it('GET /leaderboard/daily - should return daily leaderboard', async () => {
      const response = await request(app.getHttpServer())
        .get('/leaderboard/daily')
        .expect(200);

      expect(response.body).toBeDefined();
    });

    it('GET /leaderboard/weekly - should return weekly leaderboard', async () => {
      const response = await request(app.getHttpServer())
        .get('/leaderboard/weekly')
        .expect(200);

      expect(response.body).toBeDefined();
    });

    it('GET /leaderboard/monthly - should return monthly leaderboard', async () => {
      const response = await request(app.getHttpServer())
        .get('/leaderboard/monthly')
        .expect(200);

      expect(response.body).toBeDefined();
    });
  });

  describe('Authentication Protected Routes', () => {
    it('GET /leaderboard/my-rank - should return 401 without token', async () => {
      await request(app.getHttpServer())
        .get('/leaderboard/my-rank')
        .expect(401);
    });
  });

  describe('Admin Protected Routes', () => {
    it('GET /leaderboard/config - should return 401 without token', async () => {
      await request(app.getHttpServer())
        .get('/leaderboard/config')
        .expect(401);
    });

    it('POST /leaderboard/config/switch - should return 401 without token', async () => {
      await request(app.getHttpServer())
        .post('/leaderboard/config/switch')
        .send({ type: 'daily', enabled: true })
        .expect(401);
    });
  });

  describe('Swagger Documentation', () => {
    it('/api-docs (GET) - should return swagger UI', async () => {
      const response = await request(app.getHttpServer())
        .get('/api-docs')
        .expect(200);

      expect(response.text).toContain('html');
    });

    it('/api-docs-json (GET) - should return swagger JSON', async () => {
      const response = await request(app.getHttpServer())
        .get('/api-docs-json')
        .expect(200);

      expect(response.body).toHaveProperty('openapi');
      expect(response.body.info.title).toContain('Leaderboard');
    });
  });
});
```

## 5. Docker 容器化测试

### 5.1 测试环境 Docker Compose

```yaml
# docker-compose.test.yml
version: '3.8'

services:
  postgres-test:
    image: postgres:15-alpine
    container_name: leaderboard-postgres-test
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: leaderboard_test_db
    ports:
      - "5433:5432"
    tmpfs:
      - /var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s
      timeout: 3s
      retries: 10

  redis-test:
    image: redis:7-alpine
    container_name: leaderboard-redis-test
    ports:
      - "6380:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 3s
      retries: 10

  test-runner:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
    container_name: leaderboard-test-runner
    depends_on:
      postgres-test:
        condition: service_healthy
      redis-test:
        condition: service_healthy
    environment:
      NODE_ENV: test
      DATABASE_URL: postgresql://postgres:postgres@postgres-test:5432/leaderboard_test_db
      REDIS_HOST: redis-test
      REDIS_PORT: 6379
      JWT_SECRET: test-jwt-secret
    volumes:
      - ./coverage:/app/coverage
    command: >
      sh -c "npx prisma migrate deploy && npm test -- --coverage"
```

### 5.2 使用 Makefile 运行测试

```makefile
# Makefile
.PHONY: test test-unit test-integration test-e2e test-docker-unit test-docker-all

# 本地测试
test: test-unit

test-unit:
	npm test

test-integration:
	npm run test:integration

test-e2e:
	npm run test:e2e

test-cov:
	npm run test:cov

# Docker 测试
test-docker-unit:
	docker compose -f docker-compose.test.yml up --build --abort-on-container-exit test-runner
	docker compose -f docker-compose.test.yml down -v

test-docker-integration:
	docker compose -f docker-compose.test.yml up --build --abort-on-container-exit integration-test-runner
	docker compose -f docker-compose.test.yml down -v

test-docker-e2e:
	docker compose -f docker-compose.test.yml up --build --abort-on-container-exit e2e-test-runner
	docker compose -f docker-compose.test.yml down -v

test-docker-all: test-docker-unit test-docker-integration test-docker-e2e
```

### 5.3 运行 Docker 测试

```bash
# 单元测试
make test-docker-unit

# 集成测试
make test-docker-integration

# E2E 测试
make test-docker-e2e

# 所有测试
make test-docker-all
```

## 6. 手动测试指南

### 6.1 使用 cURL 测试

```bash
# 健康检查
curl http://localhost:3000/health

# 获取日榜
curl http://localhost:3000/leaderboard/daily

# 获取周榜
curl http://localhost:3000/leaderboard/weekly?limit=10

# 带认证的请求
curl -H "Authorization: Bearer <token>" \
  http://localhost:3000/leaderboard/my-rank

# 管理员操作
curl -X POST \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"type": "daily", "enabled": false}' \
  http://localhost:3000/leaderboard/config/switch
```

### 6.2 使用 VS Code REST Client

创建 `test.http` 文件：

```http
### 健康检查
GET http://localhost:3000/health

### 获取日榜
GET http://localhost:3000/leaderboard/daily?limit=10

### 获取周榜
GET http://localhost:3000/leaderboard/weekly

### 获取月榜
GET http://localhost:3000/leaderboard/monthly

### 获取我的排名 (需要 token)
GET http://localhost:3000/leaderboard/my-rank
Authorization: Bearer {{token}}

### 获取配置 (管理员)
GET http://localhost:3000/leaderboard/config
Authorization: Bearer {{adminToken}}

### 更新榜单开关 (管理员)
POST http://localhost:3000/leaderboard/config/switch
Authorization: Bearer {{adminToken}}
Content-Type: application/json

{
  "type": "daily",
  "enabled": false
}

### 手动刷新排行榜 (管理员)
POST http://localhost:3000/leaderboard/config/refresh
Authorization: Bearer {{adminToken}}
Content-Type: application/json

{
  "type": "DAILY"
}
```

### 6.3 使用 Postman

1. 导入 OpenAPI 规范：`http://localhost:3000/api-docs-json`
2. 设置环境变量：
   - `baseUrl`: `http://localhost:3000`
   - `token`: 用户 JWT token
   - `adminToken`: 管理员 JWT token

## 7. 测试最佳实践

### 7.1 测试命名规范

```typescript
describe('被测试的类/函数', () => {
  describe('方法名', () => {
    it('应该做什么（正常情况）', () => {});
    it('当什么条件时应该如何（边界情况）', () => {});
    it('什么情况应该抛出错误（异常情况）', () => {});
  });
});
```

### 7.2 AAA 模式

```typescript
it('应该正确计算分值', () => {
  // Arrange - 准备
  const totalTeam = 200;
  const maxDirect = 50;

  // Act - 执行
  const score = RankingScore.calculate(totalTeam, maxDirect);

  // Assert - 断言
  expect(score.effectiveScore).toBe(150);
});
```

### 7.3 Mock 使用

```typescript
// 创建 Mock
const mockRepository = {
  findById: jest.fn(),
  save: jest.fn(),
};

// 设置返回值
mockRepository.findById.mockResolvedValue(mockAggregate);

// 验证调用
expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
  id: expectedId,
}));
```

### 7.4 测试隔离

```typescript
beforeEach(async () => {
  // 每个测试前清理数据
  await global.testUtils.cleanDatabase();
});

afterEach(() => {
  // 清理 mock
  jest.clearAllMocks();
});
```

## 8. CI/CD 集成

### 8.1 GitHub Actions 示例

```yaml
# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: leaderboard_test_db
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Generate Prisma Client
        run: npx prisma generate

      - name: Run database migrations
        run: npx prisma db push
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/leaderboard_test_db

      - name: Run unit tests
        run: npm test -- --coverage

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/leaderboard_test_db
          REDIS_HOST: localhost
          REDIS_PORT: 6379

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
```

## 9. 测试报告

### 9.1 当前测试结果摘要

| 测试类型 | 测试数量 | 通过 | 失败 | 覆盖率 |
|----------|----------|------|------|--------|
| 单元测试 | 72 | 72 | 0 | ~88% (核心领域) |
| 集成测试 | 7 | 7 | 0 | - |
| E2E 测试 | 11 | 11 | 0 | - |
| Docker 测试 | 79 | 79 | 0 | ~20% (全量) |

### 9.2 覆盖率详情

```
领域层覆盖率:
- value-objects: 88.72%
- aggregates/leaderboard-config: 87.69%
- services/ranking-merger: 96.87%
```
