889 lines
22 KiB
Markdown
889 lines
22 KiB
Markdown
# 测试指南
|
|
|
|
## 1. 测试策略概述
|
|
|
|
### 1.1 测试金字塔
|
|
|
|
```
|
|
┌───────────┐
|
|
│ E2E │ ← 端到端测试 (少量)
|
|
│ Tests │
|
|
─┴───────────┴─
|
|
┌───────────────┐
|
|
│ Integration │ ← 集成测试 (中等)
|
|
│ Tests │
|
|
─┴───────────────┴─
|
|
┌───────────────────┐
|
|
│ Unit Tests │ ← 单元测试 (大量)
|
|
└───────────────────┘
|
|
```
|
|
|
|
### 1.2 测试类型分布
|
|
|
|
| 测试类型 | 数量 | 覆盖范围 | 执行时间 |
|
|
|---------|------|---------|---------|
|
|
| 单元测试 | 20+ | 领域层、值对象、聚合根 | ~2s |
|
|
| 集成测试 | 11+ | 数据库仓储、应用服务 | ~10s |
|
|
| E2E 测试 | 12+ | HTTP API、完整流程 | ~15s |
|
|
| Docker 测试 | 31+ | 容器化环境全量测试 | ~60s |
|
|
|
|
## 2. 单元测试
|
|
|
|
### 2.1 测试范围
|
|
|
|
单元测试主要覆盖:
|
|
- **值对象 (Value Objects)**: 不可变性、验证逻辑、相等性比较
|
|
- **聚合根 (Aggregates)**: 业务规则、状态变更、领域事件
|
|
- **领域服务**: 业务逻辑计算
|
|
|
|
### 2.2 目录结构
|
|
|
|
```
|
|
src/
|
|
├── domain/
|
|
│ ├── value-objects/
|
|
│ │ ├── date-range.vo.ts
|
|
│ │ ├── date-range.spec.ts ← 值对象单元测试
|
|
│ │ ├── report-period.vo.ts
|
|
│ │ ├── report-period.spec.ts
|
|
│ │ ├── export-format.vo.ts
|
|
│ │ └── export-format.spec.ts
|
|
│ └── aggregates/
|
|
│ ├── report-definition/
|
|
│ │ ├── report-definition.aggregate.ts
|
|
│ │ └── report-definition.spec.ts ← 聚合根单元测试
|
|
│ └── report-snapshot/
|
|
│ ├── report-snapshot.aggregate.ts
|
|
│ └── report-snapshot.spec.ts
|
|
```
|
|
|
|
### 2.3 值对象测试示例
|
|
|
|
```typescript
|
|
// src/domain/value-objects/date-range.spec.ts
|
|
describe('DateRange Value Object', () => {
|
|
describe('create', () => {
|
|
it('should create valid date range', () => {
|
|
const startDate = new Date('2024-01-01');
|
|
const endDate = new Date('2024-01-31');
|
|
|
|
const dateRange = DateRange.create(startDate, endDate);
|
|
|
|
expect(dateRange.startDate).toEqual(startDate);
|
|
expect(dateRange.endDate).toEqual(endDate);
|
|
});
|
|
|
|
it('should throw error when start date is after end date', () => {
|
|
const startDate = new Date('2024-01-31');
|
|
const endDate = new Date('2024-01-01');
|
|
|
|
expect(() => DateRange.create(startDate, endDate))
|
|
.toThrow('Start date cannot be after end date');
|
|
});
|
|
});
|
|
|
|
describe('equals', () => {
|
|
it('should return true for equal date ranges', () => {
|
|
const range1 = DateRange.create(
|
|
new Date('2024-01-01'),
|
|
new Date('2024-01-31')
|
|
);
|
|
const range2 = DateRange.create(
|
|
new Date('2024-01-01'),
|
|
new Date('2024-01-31')
|
|
);
|
|
|
|
expect(range1.equals(range2)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getDays', () => {
|
|
it('should calculate correct number of days', () => {
|
|
const dateRange = DateRange.create(
|
|
new Date('2024-01-01'),
|
|
new Date('2024-01-31')
|
|
);
|
|
|
|
expect(dateRange.getDays()).toBe(31);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 2.4 聚合根测试示例
|
|
|
|
```typescript
|
|
// src/domain/aggregates/report-snapshot/report-snapshot.spec.ts
|
|
describe('ReportSnapshot Aggregate', () => {
|
|
const mockDefinition = ReportDefinition.reconstitute({
|
|
id: 1n,
|
|
code: 'RPT_TEST',
|
|
name: 'Test Report',
|
|
description: 'Test description',
|
|
category: 'TEST',
|
|
dataSource: 'test_table',
|
|
isActive: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create snapshot with pending status', () => {
|
|
const snapshot = ReportSnapshot.create({
|
|
definition: mockDefinition,
|
|
period: ReportPeriod.DAILY,
|
|
dateRange: DateRange.create(
|
|
new Date('2024-01-01'),
|
|
new Date('2024-01-01')
|
|
),
|
|
});
|
|
|
|
expect(snapshot.status).toBe(SnapshotStatus.PENDING);
|
|
expect(snapshot.reportCode).toBe('RPT_TEST');
|
|
});
|
|
});
|
|
|
|
describe('markAsProcessing', () => {
|
|
it('should transition from pending to processing', () => {
|
|
const snapshot = createPendingSnapshot();
|
|
|
|
snapshot.markAsProcessing();
|
|
|
|
expect(snapshot.status).toBe(SnapshotStatus.PROCESSING);
|
|
});
|
|
|
|
it('should throw error if not in pending status', () => {
|
|
const snapshot = createCompletedSnapshot();
|
|
|
|
expect(() => snapshot.markAsProcessing())
|
|
.toThrow('Cannot start processing');
|
|
});
|
|
});
|
|
|
|
describe('complete', () => {
|
|
it('should set data and mark as completed', () => {
|
|
const snapshot = createProcessingSnapshot();
|
|
const data = { items: [{ id: 1 }], total: 1 };
|
|
|
|
snapshot.complete(data);
|
|
|
|
expect(snapshot.status).toBe(SnapshotStatus.COMPLETED);
|
|
expect(snapshot.snapshotData).toEqual(data);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 2.5 运行单元测试
|
|
|
|
```bash
|
|
# 运行所有单元测试
|
|
npm test
|
|
|
|
# 运行特定文件
|
|
npm test -- date-range.spec.ts
|
|
|
|
# Watch 模式
|
|
npm run test:watch
|
|
|
|
# 带覆盖率
|
|
npm run test:cov
|
|
```
|
|
|
|
## 3. 集成测试
|
|
|
|
### 3.1 测试范围
|
|
|
|
集成测试覆盖:
|
|
- **仓储实现**: Prisma 数据库操作
|
|
- **应用服务**: 命令/查询处理
|
|
- **缓存服务**: Redis 缓存操作
|
|
|
|
### 3.2 目录结构
|
|
|
|
```
|
|
src/
|
|
├── infrastructure/
|
|
│ └── repositories/
|
|
│ ├── prisma-report-definition.repository.ts
|
|
│ └── prisma-report-definition.repository.integration.spec.ts
|
|
├── application/
|
|
│ └── services/
|
|
│ ├── reporting-application.service.ts
|
|
│ └── reporting-application.service.integration.spec.ts
|
|
```
|
|
|
|
### 3.3 数据库集成测试
|
|
|
|
```typescript
|
|
// prisma-report-definition.repository.integration.spec.ts
|
|
describe('PrismaReportDefinitionRepository (Integration)', () => {
|
|
let repository: PrismaReportDefinitionRepository;
|
|
let prismaService: PrismaService;
|
|
|
|
beforeAll(async () => {
|
|
const module = await Test.createTestingModule({
|
|
providers: [
|
|
PrismaReportDefinitionRepository,
|
|
PrismaService,
|
|
],
|
|
}).compile();
|
|
|
|
repository = module.get(PrismaReportDefinitionRepository);
|
|
prismaService = module.get(PrismaService);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// 清理测试数据
|
|
await prismaService.reportSnapshot.deleteMany();
|
|
await prismaService.reportDefinition.deleteMany();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prismaService.$disconnect();
|
|
});
|
|
|
|
describe('save', () => {
|
|
it('should persist report definition to database', async () => {
|
|
const definition = ReportDefinition.create({
|
|
code: 'RPT_TEST',
|
|
name: 'Test Report',
|
|
description: 'Test',
|
|
category: 'TEST',
|
|
dataSource: 'test_table',
|
|
});
|
|
|
|
await repository.save(definition);
|
|
|
|
const found = await prismaService.reportDefinition.findUnique({
|
|
where: { code: 'RPT_TEST' },
|
|
});
|
|
expect(found).not.toBeNull();
|
|
expect(found?.name).toBe('Test Report');
|
|
});
|
|
});
|
|
|
|
describe('findByCode', () => {
|
|
it('should return null for non-existent code', async () => {
|
|
const result = await repository.findByCode('NON_EXISTENT');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return definition when exists', async () => {
|
|
// 创建测试数据
|
|
await prismaService.reportDefinition.create({
|
|
data: {
|
|
code: 'RPT_FIND',
|
|
name: 'Find Test',
|
|
description: 'Test',
|
|
category: 'TEST',
|
|
dataSource: 'test_table',
|
|
isActive: true,
|
|
},
|
|
});
|
|
|
|
const result = await repository.findByCode('RPT_FIND');
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.code).toBe('RPT_FIND');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 3.4 应用服务集成测试
|
|
|
|
```typescript
|
|
// reporting-application.service.integration.spec.ts
|
|
describe('ReportingApplicationService (Integration)', () => {
|
|
let service: ReportingApplicationService;
|
|
let prismaService: PrismaService;
|
|
|
|
beforeAll(async () => {
|
|
const module = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
|
|
service = module.get(ReportingApplicationService);
|
|
prismaService = module.get(PrismaService);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await cleanDatabase(prismaService);
|
|
await seedTestData(prismaService);
|
|
});
|
|
|
|
describe('generateReport', () => {
|
|
it('should create snapshot for valid report', async () => {
|
|
const result = await service.generateReport({
|
|
reportCode: 'RPT_LEADERBOARD',
|
|
reportPeriod: ReportPeriod.DAILY,
|
|
startDate: new Date('2024-01-01'),
|
|
endDate: new Date('2024-01-01'),
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.status).toBe(SnapshotStatus.COMPLETED);
|
|
});
|
|
|
|
it('should throw when report definition not found', async () => {
|
|
await expect(
|
|
service.generateReport({
|
|
reportCode: 'NON_EXISTENT',
|
|
reportPeriod: ReportPeriod.DAILY,
|
|
startDate: new Date('2024-01-01'),
|
|
endDate: new Date('2024-01-01'),
|
|
})
|
|
).rejects.toThrow(ReportDefinitionNotFoundException);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 3.5 测试数据库配置
|
|
|
|
```env
|
|
# .env.test
|
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/rwadurian_reporting_test?schema=public
|
|
REDIS_HOST=localhost
|
|
REDIS_PORT=6380
|
|
```
|
|
|
|
### 3.6 运行集成测试
|
|
|
|
```bash
|
|
# 启动测试依赖
|
|
docker compose -f docker-compose.test.yml up -d postgres-test redis-test
|
|
|
|
# 推送 Schema
|
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/rwadurian_reporting_test?schema=public" \
|
|
npx prisma db push
|
|
|
|
# 运行集成测试
|
|
npm run test:integration
|
|
|
|
# 或使用 Makefile
|
|
make test-integration
|
|
```
|
|
|
|
## 4. E2E 测试
|
|
|
|
### 4.1 测试范围
|
|
|
|
E2E 测试覆盖:
|
|
- HTTP API 端点
|
|
- 请求验证
|
|
- 响应格式
|
|
- 错误处理
|
|
- 完整业务流程
|
|
|
|
### 4.2 目录结构
|
|
|
|
```
|
|
test/
|
|
├── app.e2e-spec.ts # 主 E2E 测试文件
|
|
├── jest-e2e.json # E2E Jest 配置
|
|
└── setup-e2e.ts # E2E 测试设置
|
|
```
|
|
|
|
### 4.3 E2E 测试配置
|
|
|
|
```json
|
|
// test/jest-e2e.json
|
|
{
|
|
"moduleFileExtensions": ["js", "json", "ts"],
|
|
"rootDir": ".",
|
|
"testEnvironment": "node",
|
|
"testRegex": ".e2e-spec.ts$",
|
|
"testTimeout": 30000,
|
|
"transform": {
|
|
"^.+\\.(t|j)s$": "ts-jest"
|
|
},
|
|
"setupFilesAfterEnv": ["<rootDir>/setup-e2e.ts"],
|
|
"moduleNameMapper": {
|
|
"^@/(.*)$": "<rootDir>/../src/$1",
|
|
"^@domain/(.*)$": "<rootDir>/../src/domain/$1",
|
|
"^@application/(.*)$": "<rootDir>/../src/application/$1",
|
|
"^@infrastructure/(.*)$": "<rootDir>/../src/infrastructure/$1",
|
|
"^@api/(.*)$": "<rootDir>/../src/api/$1"
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// test/setup-e2e.ts
|
|
import * as dotenv from 'dotenv';
|
|
import * as path from 'path';
|
|
|
|
dotenv.config({ path: path.resolve(__dirname, '../.env.test') });
|
|
```
|
|
|
|
### 4.4 E2E 测试示例
|
|
|
|
```typescript
|
|
// test/app.e2e-spec.ts
|
|
describe('Reporting Service (e2e)', () => {
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
app.setGlobalPrefix('api/v1');
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: true,
|
|
forbidNonWhitelisted: true,
|
|
transform: true,
|
|
}),
|
|
);
|
|
// 注意: TransformInterceptor 已在 AppModule 中注册
|
|
// 不要重复注册,避免响应双重包装
|
|
await app.init();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('Health Check', () => {
|
|
it('/api/v1/health (GET) should return ok status', () => {
|
|
return request(app.getHttpServer())
|
|
.get('/api/v1/health')
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
expect(res.body.data.status).toBe('ok');
|
|
expect(res.body.data.service).toBe('reporting-service');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Reports API', () => {
|
|
describe('POST /api/v1/reports/generate', () => {
|
|
it('should validate request body', () => {
|
|
return request(app.getHttpServer())
|
|
.post('/api/v1/reports/generate')
|
|
.send({})
|
|
.expect(400);
|
|
});
|
|
|
|
it('should reject invalid report period', () => {
|
|
return request(app.getHttpServer())
|
|
.post('/api/v1/reports/generate')
|
|
.send({
|
|
reportCode: 'RPT_LEADERBOARD',
|
|
reportPeriod: 'INVALID_PERIOD',
|
|
startDate: '2024-01-01',
|
|
endDate: '2024-01-31',
|
|
})
|
|
.expect(400);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Export API', () => {
|
|
describe('POST /api/v1/export', () => {
|
|
it('should validate request body', () => {
|
|
return request(app.getHttpServer())
|
|
.post('/api/v1/export')
|
|
.send({})
|
|
.expect(400);
|
|
});
|
|
|
|
it('should reject invalid format', () => {
|
|
return request(app.getHttpServer())
|
|
.post('/api/v1/export')
|
|
.send({
|
|
snapshotId: '1',
|
|
format: 'INVALID_FORMAT',
|
|
})
|
|
.expect(400);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 4.5 运行 E2E 测试
|
|
|
|
```bash
|
|
# 确保测试数据库运行
|
|
docker compose -f docker-compose.test.yml up -d postgres-test redis-test
|
|
|
|
# 运行 E2E 测试
|
|
npm run test:e2e
|
|
|
|
# 或使用 Makefile
|
|
make test-e2e
|
|
```
|
|
|
|
## 5. Docker 测试
|
|
|
|
### 5.1 测试架构
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Docker Network │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ PostgreSQL │ │ Redis │ │ Service │ │
|
|
│ │ :5432 │ │ :6379 │ │ Test │ │
|
|
│ │ (tmpfs) │ │ │ │ Container │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 5.2 Docker Compose 配置
|
|
|
|
```yaml
|
|
# docker-compose.test.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
postgres-test:
|
|
image: postgres:15-alpine
|
|
container_name: reporting-postgres-test
|
|
environment:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: postgres
|
|
POSTGRES_DB: rwadurian_reporting_test
|
|
ports:
|
|
- "5433:5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
tmpfs:
|
|
- /var/lib/postgresql/data # 使用内存存储加速测试
|
|
|
|
redis-test:
|
|
image: redis:7-alpine
|
|
container_name: reporting-redis-test
|
|
ports:
|
|
- "6380:6379"
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
reporting-service-test:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.test
|
|
container_name: reporting-service-test
|
|
depends_on:
|
|
postgres-test:
|
|
condition: service_healthy
|
|
redis-test:
|
|
condition: service_healthy
|
|
environment:
|
|
NODE_ENV: test
|
|
DATABASE_URL: postgresql://postgres:postgres@postgres-test:5432/rwadurian_reporting_test?schema=public
|
|
REDIS_HOST: redis-test
|
|
REDIS_PORT: 6379
|
|
JWT_SECRET: test-secret-key
|
|
volumes:
|
|
- ./coverage:/app/coverage
|
|
command: sh -c "npx prisma db push --skip-generate && npm run test:cov"
|
|
```
|
|
|
|
### 5.3 测试 Dockerfile
|
|
|
|
```dockerfile
|
|
# Dockerfile.test
|
|
FROM node:20-alpine
|
|
|
|
WORKDIR /app
|
|
|
|
# 安装 OpenSSL (Prisma 依赖)
|
|
RUN apk add --no-cache openssl openssl-dev
|
|
|
|
# 安装依赖
|
|
COPY package*.json ./
|
|
RUN npm ci
|
|
|
|
# 复制 Prisma Schema
|
|
COPY prisma ./prisma/
|
|
|
|
# 生成 Prisma Client
|
|
RUN npx prisma generate
|
|
|
|
# 复制源代码
|
|
COPY . .
|
|
|
|
# 构建应用
|
|
RUN npm run build
|
|
|
|
# 默认运行测试
|
|
CMD ["npm", "test"]
|
|
```
|
|
|
|
### 5.4 .dockerignore
|
|
|
|
```
|
|
node_modules
|
|
dist
|
|
coverage
|
|
.git
|
|
.env*
|
|
!.env.example
|
|
*.log
|
|
.claude/
|
|
```
|
|
|
|
### 5.5 运行 Docker 测试
|
|
|
|
```bash
|
|
# 构建并运行所有测试
|
|
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
|
|
|
|
# 查看测试结果
|
|
docker compose -f docker-compose.test.yml logs reporting-service-test
|
|
|
|
# 清理
|
|
docker compose -f docker-compose.test.yml down -v
|
|
|
|
# 使用 Makefile
|
|
make test-docker-all
|
|
```
|
|
|
|
## 6. Makefile 命令
|
|
|
|
```makefile
|
|
# Makefile
|
|
.PHONY: test test-unit test-integration test-e2e test-docker-all
|
|
|
|
# 运行所有测试
|
|
test: test-unit test-integration test-e2e
|
|
|
|
# 单元测试
|
|
test-unit:
|
|
npm test
|
|
|
|
# 集成测试
|
|
test-integration:
|
|
npm run test:integration
|
|
|
|
# E2E 测试
|
|
test-e2e:
|
|
npm run test:e2e
|
|
|
|
# Docker 中运行所有测试
|
|
test-docker-all:
|
|
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
|
|
docker compose -f docker-compose.test.yml down -v
|
|
|
|
# 测试覆盖率
|
|
test-cov:
|
|
npm run test:cov
|
|
|
|
# 清理测试容器
|
|
test-clean:
|
|
docker compose -f docker-compose.test.yml down -v
|
|
```
|
|
|
|
## 7. 测试最佳实践
|
|
|
|
### 7.1 测试命名规范
|
|
|
|
```typescript
|
|
describe('ClassName', () => {
|
|
describe('methodName', () => {
|
|
it('should [expected behavior] when [condition]', () => {
|
|
// test implementation
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 7.2 AAA 模式
|
|
|
|
```typescript
|
|
it('should create valid date range', () => {
|
|
// Arrange - 准备测试数据
|
|
const startDate = new Date('2024-01-01');
|
|
const endDate = new Date('2024-01-31');
|
|
|
|
// Act - 执行被测试的操作
|
|
const dateRange = DateRange.create(startDate, endDate);
|
|
|
|
// Assert - 验证结果
|
|
expect(dateRange.startDate).toEqual(startDate);
|
|
expect(dateRange.endDate).toEqual(endDate);
|
|
});
|
|
```
|
|
|
|
### 7.3 测试隔离
|
|
|
|
```typescript
|
|
describe('Repository Integration Tests', () => {
|
|
beforeEach(async () => {
|
|
// 每个测试前清理数据
|
|
await prismaService.reportSnapshot.deleteMany();
|
|
await prismaService.reportDefinition.deleteMany();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// 测试结束后断开连接
|
|
await prismaService.$disconnect();
|
|
});
|
|
});
|
|
```
|
|
|
|
### 7.4 Mock 使用
|
|
|
|
```typescript
|
|
// 使用 Jest mock
|
|
const mockRepository = {
|
|
findByCode: jest.fn(),
|
|
save: jest.fn(),
|
|
findAll: jest.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('should call repository with correct parameters', async () => {
|
|
mockRepository.findByCode.mockResolvedValue(mockDefinition);
|
|
|
|
await service.getReportDefinition('RPT_TEST');
|
|
|
|
expect(mockRepository.findByCode).toHaveBeenCalledWith('RPT_TEST');
|
|
});
|
|
```
|
|
|
|
## 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: rwadurian_reporting_test
|
|
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
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Generate Prisma Client
|
|
run: npx prisma generate
|
|
|
|
- name: Push database schema
|
|
run: npx prisma db push
|
|
env:
|
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/rwadurian_reporting_test?schema=public
|
|
|
|
- name: Run unit tests
|
|
run: npm test
|
|
|
|
- name: Run integration tests
|
|
run: npm run test:integration
|
|
env:
|
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/rwadurian_reporting_test?schema=public
|
|
REDIS_HOST: localhost
|
|
REDIS_PORT: 6379
|
|
|
|
- name: Run E2E tests
|
|
run: npm run test:e2e
|
|
env:
|
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/rwadurian_reporting_test?schema=public
|
|
REDIS_HOST: localhost
|
|
REDIS_PORT: 6379
|
|
|
|
- name: Upload coverage
|
|
uses: codecov/codecov-action@v3
|
|
with:
|
|
files: ./coverage/lcov.info
|
|
```
|
|
|
|
## 9. 故障排除
|
|
|
|
### 9.1 常见问题
|
|
|
|
**Q: 测试找不到模块**
|
|
```bash
|
|
# 重新生成 Prisma Client
|
|
npx prisma generate
|
|
```
|
|
|
|
**Q: 数据库连接失败**
|
|
```bash
|
|
# 检查容器状态
|
|
docker ps
|
|
|
|
# 检查数据库日志
|
|
docker logs reporting-postgres-test
|
|
```
|
|
|
|
**Q: E2E 测试响应格式错误**
|
|
```typescript
|
|
// 确保不要重复注册 TransformInterceptor
|
|
// AppModule 中已注册,测试中不需要再次注册
|
|
```
|
|
|
|
**Q: Docker 测试 Prisma 错误**
|
|
```dockerfile
|
|
# 确保 Dockerfile.test 包含 OpenSSL
|
|
RUN apk add --no-cache openssl openssl-dev
|
|
```
|
|
|
|
### 9.2 调试测试
|
|
|
|
```bash
|
|
# 运行单个测试文件
|
|
npm test -- --testPathPattern=date-range.spec.ts
|
|
|
|
# 详细输出
|
|
npm test -- --verbose
|
|
|
|
# 调试模式
|
|
node --inspect-brk node_modules/.bin/jest --runInBand
|
|
```
|