rwadurian/backend/services/admin-service/docs/TESTING.md

1197 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Admin Service 测试文档
## 目录
- [1. 测试架构概述](#1-测试架构概述)
- [2. 测试环境设置](#2-测试环境设置)
- [3. 单元测试](#3-单元测试)
- [4. 集成测试](#4-集成测试)
- [5. E2E 测试](#5-e2e-测试)
- [6. 测试执行](#6-测试执行)
- [7. 测试覆盖率](#7-测试覆盖率)
- [8. CI/CD 集成](#8-cicd-集成)
---
## 1. 测试架构概述
### 1.1 三层测试金字塔
```
╱╲
E2E╲ (15 tests) - API 端点、完整业务流程
╱──────╲
Integration (21 tests) - Repository、Handler
╱──────────╲
Unit Tests ╲ (53 tests) - 值对象、实体、映射器
╲──────────────╱
总计: ~89 测试用例
```
### 1.2 测试技术栈
| 工具 | 版本 | 用途 |
|-----|------|------|
| **Jest** | 29.5.0 | 测试框架 |
| **ts-jest** | 29.1.0 | TypeScript 支持 |
| **Supertest** | 6.3.3 | HTTP 请求测试 |
| **@nestjs/testing** | 10.0.0 | NestJS 测试工具 |
| **Prisma** | 5.7.0 | 数据库 ORM |
| **PostgreSQL** | 16-alpine | 测试数据库 |
### 1.3 测试原则
1. **测试金字塔**: 单元测试多、集成测试适中、E2E 测试少
2. **独立性**: 每个测试用例独立运行,不依赖其他测试
3. **可重复性**: 测试结果稳定,多次运行结果一致
4. **快速反馈**: 单元测试秒级完成,集成测试分钟级
5. **有意义**: 测试业务逻辑,而非框架行为
---
## 2. 测试环境设置
### 2.1 本地开发环境
#### 安装依赖
```bash
cd backend/services/admin-service
npm install
```
#### 配置测试环境变量
创建 `.env.test`:
```env
NODE_ENV=test
APP_PORT=3005
API_PREFIX=api/v1
DATABASE_URL=postgresql://postgres:password@localhost:5433/admin_service_test?schema=public
```
#### 启动测试数据库
**方案 1: Docker**
```bash
docker run -d \
--name admin-test-db \
--rm \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=admin_service_test \
-p 5433:5432 \
postgres:16-alpine
```
**方案 2: 本地 PostgreSQL**
```bash
psql -U postgres -c "CREATE DATABASE admin_service_test;"
```
#### 运行数据库迁移
```bash
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npx prisma migrate deploy
```
### 2.2 WSL2 环境 (推荐)
#### 一键启动脚本
```bash
# 使用自动化脚本
chmod +x scripts/test-with-docker-db.sh
./scripts/test-with-docker-db.sh
```
**脚本内容** (`scripts/test-with-docker-db.sh`):
```bash
#!/bin/bash
echo "=== Admin Service Test Suite ==="
# 1. 启动数据库
echo "1. Starting PostgreSQL..."
docker run -d --name admin-test-db --rm \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=admin_service_test \
-p 5433:5432 \
postgres:16-alpine
sleep 5
# 2. 运行迁移
echo "2. Running migrations..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npx prisma migrate deploy
# 3. 生成 Prisma Client
echo "3. Generating Prisma client..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run prisma:generate
# 4. 运行测试
echo "4. Running tests..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm test
# 5. 生成覆盖率报告
echo "5. Generating coverage report..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:cov
# 6. 清理
echo "6. Cleaning up..."
docker stop admin-test-db
echo "=== Test suite completed! ==="
```
### 2.3 Docker 测试环境
#### 使用 Docker Compose
```bash
# 启动测试环境
docker-compose -f docker-compose.test.yml up --build
# 查看测试结果
docker-compose -f docker-compose.test.yml logs admin-service-test
# 清理
docker-compose -f docker-compose.test.yml down -v
```
**docker-compose.test.yml**:
```yaml
version: '3.8'
services:
postgres-test:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: admin_service_test
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
admin-service-test:
build:
context: .
dockerfile: Dockerfile.test
environment:
DATABASE_URL: postgresql://postgres:password@postgres-test:5432/admin_service_test
depends_on:
postgres-test:
condition: service_healthy
command: npm test
```
---
## 3. 单元测试
### 3.1 单元测试结构
```
test/unit/
├── domain/
│ ├── value-objects/ # 值对象测试
│ │ ├── version-code.vo.spec.ts
│ │ ├── version-name.vo.spec.ts
│ │ ├── file-size.vo.spec.ts
│ │ └── file-sha256.vo.spec.ts
│ └── entities/ # 实体测试
│ └── app-version.entity.spec.ts
└── infrastructure/
└── mappers/ # 映射器测试
└── app-version.mapper.spec.ts
```
### 3.2 值对象测试示例
**测试文件**: `test/unit/domain/value-objects/version-code.vo.spec.ts`
```typescript
import { VersionCode } from '@/domain/value-objects/version-code.vo';
import { DomainException } from '@/shared/exceptions/domain.exception';
describe('VersionCode Value Object', () => {
describe('创建版本号', () => {
it('应该成功创建有效的版本号', () => {
const versionCode = VersionCode.create(100);
expect(versionCode).toBeDefined();
expect(versionCode.value).toBe(100);
});
it('应该拒绝负数版本号', () => {
expect(() => VersionCode.create(-1)).toThrow(DomainException);
});
it('应该拒绝非整数版本号', () => {
expect(() => VersionCode.create(1.5)).toThrow(DomainException);
});
it('应该拒绝零作为版本号', () => {
expect(() => VersionCode.create(0)).toThrow(DomainException);
});
});
describe('版本号比较', () => {
it('应该正确比较大于关系', () => {
const v100 = VersionCode.create(100);
const v99 = VersionCode.create(99);
expect(v100.isGreaterThan(v99)).toBe(true);
expect(v99.isGreaterThan(v100)).toBe(false);
});
it('应该正确比较小于关系', () => {
const v100 = VersionCode.create(100);
const v101 = VersionCode.create(101);
expect(v100.isLessThan(v101)).toBe(true);
expect(v101.isLessThan(v100)).toBe(false);
});
it('应该正确判断相等', () => {
const v1 = VersionCode.create(100);
const v2 = VersionCode.create(100);
expect(v1.equals(v2)).toBe(true);
});
});
describe('边界条件', () => {
it('应该支持最小版本号 1', () => {
expect(() => VersionCode.create(1)).not.toThrow();
});
it('应该支持大版本号', () => {
expect(() => VersionCode.create(99999)).not.toThrow();
});
});
});
```
### 3.3 实体测试示例
**测试文件**: `test/unit/domain/entities/app-version.entity.spec.ts`
```typescript
import { AppVersion } from '@/domain/entities/app-version.entity';
import { Platform } from '@/domain/enums/platform.enum';
import { DomainException } from '@/shared/exceptions/domain.exception';
describe('AppVersion Entity', () => {
const createValidParams = () => ({
platform: Platform.ANDROID,
versionCode: 100,
versionName: '1.0.0',
buildNumber: '1',
downloadUrl: 'https://example.com/app.apk',
fileSize: 52428800n,
fileSha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
changelog: 'Initial release',
isEnabled: true,
isForceUpdate: false,
createdBy: 'admin',
});
describe('创建实体', () => {
it('应该成功创建有效的版本实体', () => {
const version = AppVersion.create(createValidParams());
expect(version).toBeDefined();
expect(version.platform).toBe(Platform.ANDROID);
expect(version.versionCode.value).toBe(100);
expect(version.isEnabled).toBe(true);
});
it('应该拒绝无效的版本号', () => {
const params = { ...createValidParams(), versionCode: -1 };
expect(() => AppVersion.create(params)).toThrow(DomainException);
});
it('应该拒绝无效的 SHA256', () => {
const params = { ...createValidParams(), fileSha256: 'invalid' };
expect(() => AppVersion.create(params)).toThrow(DomainException);
});
});
describe('业务方法', () => {
describe('启用版本', () => {
it('应该成功启用版本', () => {
const version = AppVersion.create({ ...createValidParams(), isEnabled: false });
version.enable('admin');
expect(version.isEnabled).toBe(true);
expect(version.updatedBy).toBe('admin');
});
});
describe('禁用版本', () => {
it('应该成功禁用版本', () => {
const version = AppVersion.create(createValidParams());
version.disable('admin');
expect(version.isEnabled).toBe(false);
expect(version.isForceUpdate).toBe(false); // 自动设置为 false
});
it('禁用时应自动取消强制更新', () => {
const version = AppVersion.create({ ...createValidParams(), isForceUpdate: true });
version.disable('admin');
expect(version.isForceUpdate).toBe(false);
});
});
describe('设置强制更新', () => {
it('应该成功设置强制更新', () => {
const version = AppVersion.create(createValidParams());
version.setForceUpdate(true, 'admin');
expect(version.isForceUpdate).toBe(true);
});
it('禁用状态不能设置强制更新', () => {
const version = AppVersion.create({ ...createValidParams(), isEnabled: false });
expect(() => version.setForceUpdate(true, 'admin')).toThrow(DomainException);
});
});
});
describe('查询方法', () => {
it('应该正确判断是否启用', () => {
const enabledVersion = AppVersion.create({ ...createValidParams(), isEnabled: true });
const disabledVersion = AppVersion.create({ ...createValidParams(), isEnabled: false });
expect(enabledVersion.isEnabledVersion()).toBe(true);
expect(disabledVersion.isEnabledVersion()).toBe(false);
});
it('应该正确判断是否强制更新', () => {
const forceVersion = AppVersion.create({ ...createValidParams(), isForceUpdate: true });
const normalVersion = AppVersion.create({ ...createValidParams(), isForceUpdate: false });
expect(forceVersion.requiresForceUpdate()).toBe(true);
expect(normalVersion.requiresForceUpdate()).toBe(false);
});
});
});
```
### 3.4 运行单元测试
```bash
# 运行所有单元测试
npm run test:unit
# 运行特定文件
npm run test:unit -- version-code.vo.spec
# 监听模式
npm run test:unit -- --watch
# 覆盖率
npm run test:unit -- --coverage
```
**预期输出**:
```
PASS test/unit/domain/value-objects/version-code.vo.spec.ts
PASS test/unit/domain/value-objects/version-name.vo.spec.ts
PASS test/unit/domain/value-objects/file-size.vo.spec.ts
PASS test/unit/domain/value-objects/file-sha256.vo.spec.ts
PASS test/unit/domain/entities/app-version.entity.spec.ts
PASS test/unit/infrastructure/mappers/app-version.mapper.spec.ts
Test Suites: 6 passed, 6 total
Tests: 53 passed, 53 total
Snapshots: 0 total
Time: 2.345 s
```
---
## 4. 集成测试
### 4.1 集成测试结构
```
test/integration/
├── repositories/ # Repository 集成测试
│ └── app-version.repository.spec.ts
└── handlers/ # Handler 集成测试
└── create-version.handler.spec.ts
```
### 4.2 Repository 集成测试示例
**测试文件**: `test/integration/repositories/app-version.repository.spec.ts`
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { AppVersionRepositoryImpl } from '@/infrastructure/persistence/repositories/app-version.repository.impl';
import { PrismaService } from '@/infrastructure/prisma/prisma.service';
import { AppVersion } from '@/domain/entities/app-version.entity';
import { Platform } from '@/domain/enums/platform.enum';
describe('AppVersionRepository (Integration)', () => {
let repository: AppVersionRepositoryImpl;
let prisma: PrismaService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AppVersionRepositoryImpl, PrismaService],
}).compile();
repository = module.get<AppVersionRepositoryImpl>(AppVersionRepositoryImpl);
prisma = module.get<PrismaService>(PrismaService);
});
beforeEach(async () => {
// 清空测试数据
await prisma.appVersion.deleteMany({});
});
afterAll(async () => {
await prisma.$disconnect();
});
const createTestVersion = (overrides = {}) => {
return AppVersion.create({
platform: Platform.ANDROID,
versionCode: 100,
versionName: '1.0.0',
buildNumber: '1',
downloadUrl: 'https://example.com/app.apk',
fileSize: 52428800n,
fileSha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
changelog: 'Test version',
isEnabled: true,
isForceUpdate: false,
createdBy: 'test',
...overrides,
});
};
describe('保存版本', () => {
it('应该成功保存新版本到数据库', async () => {
const version = createTestVersion();
const saved = await repository.save(version);
expect(saved).toBeDefined();
expect(saved.id).toBeDefined();
expect(saved.versionCode.value).toBe(100);
});
it('应该拒绝重复的版本号', async () => {
const version1 = createTestVersion();
const version2 = createTestVersion(); // 相同版本号
await repository.save(version1);
await expect(repository.save(version2)).rejects.toThrow();
});
});
describe('查询版本', () => {
it('应该通过 ID 查找版本', async () => {
const version = createTestVersion();
const saved = await repository.save(version);
const found = await repository.findById(saved.id);
expect(found).not.toBeNull();
expect(found!.id).toBe(saved.id);
});
it('应该返回 null 如果版本不存在', async () => {
const found = await repository.findById('non-existent-id');
expect(found).toBeNull();
});
it('应该查找最新启用的版本', async () => {
await repository.save(createTestVersion({ versionCode: 100 }));
await repository.save(createTestVersion({ versionCode: 101 }));
await repository.save(createTestVersion({ versionCode: 102 }));
const latest = await repository.findLatestEnabledVersion(Platform.ANDROID);
expect(latest).not.toBeNull();
expect(latest!.versionCode.value).toBe(102);
});
it('应该按平台过滤查询', async () => {
await repository.save(createTestVersion({ platform: Platform.ANDROID, versionCode: 100 }));
await repository.save(createTestVersion({ platform: Platform.IOS, versionCode: 1 }));
const androidVersions = await repository.findAll({ platform: Platform.ANDROID });
const iosVersions = await repository.findAll({ platform: Platform.IOS });
expect(androidVersions.length).toBe(1);
expect(iosVersions.length).toBe(1);
});
});
});
```
### 4.3 Handler 集成测试示例
**测试文件**: `test/integration/handlers/create-version.handler.spec.ts`
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { CreateVersionHandler } from '@/application/handlers/create-version.handler';
import { CreateVersionCommand } from '@/application/commands/create-version.command';
import { AppVersionRepositoryImpl } from '@/infrastructure/persistence/repositories/app-version.repository.impl';
import { PrismaService } from '@/infrastructure/prisma/prisma.service';
import { Platform } from '@/domain/enums/platform.enum';
import { APP_VERSION_REPOSITORY } from '@/domain/repositories/app-version.repository';
describe('CreateVersionHandler (Integration)', () => {
let handler: CreateVersionHandler;
let repository: AppVersionRepositoryImpl;
let prisma: PrismaService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CreateVersionHandler,
PrismaService,
{
provide: APP_VERSION_REPOSITORY,
useClass: AppVersionRepositoryImpl,
},
],
}).compile();
handler = module.get<CreateVersionHandler>(CreateVersionHandler);
repository = module.get<AppVersionRepositoryImpl>(APP_VERSION_REPOSITORY);
prisma = module.get<PrismaService>(PrismaService);
});
beforeEach(async () => {
await prisma.appVersion.deleteMany({});
});
afterAll(async () => {
await prisma.$disconnect();
});
it('应该成功创建 Android 版本', async () => {
const command = new CreateVersionCommand(
Platform.ANDROID,
100,
'1.0.0',
'1',
'https://example.com/app.apk',
52428800,
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
'Initial release',
true,
false,
'admin',
);
const result = await handler.execute(command);
expect(result).toBeDefined();
expect(result.platform).toBe(Platform.ANDROID);
expect(result.versionCode.value).toBe(100);
// 验证数据库持久化
const saved = await repository.findById(result.id);
expect(saved).not.toBeNull();
});
it('应该拒绝重复的版本号', async () => {
const command1 = new CreateVersionCommand(/* ... */);
await handler.execute(command1);
const command2 = new CreateVersionCommand(/* 相同版本号 */);
await expect(handler.execute(command2)).rejects.toThrow();
});
});
```
### 4.4 运行集成测试
```bash
# 运行所有集成测试
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:integration
# 运行特定文件
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:integration -- app-version.repository.spec
# 监听模式
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:integration -- --watch
```
---
## 5. E2E 测试
### 5.1 E2E 测试结构
```
test/e2e/
└── version.controller.spec.ts # API 端点完整测试
```
### 5.2 E2E 测试示例
**测试文件**: `test/e2e/version.controller.spec.ts`
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '@/app.module';
import { PrismaService } from '@/infrastructure/prisma/prisma.service';
describe('VersionController (E2E)', () => {
let app: INestApplication;
let prisma: PrismaService;
const apiPrefix = 'api/v1';
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.setGlobalPrefix(apiPrefix);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.init();
prisma = moduleFixture.get<PrismaService>(PrismaService);
});
beforeEach(async () => {
await prisma.appVersion.deleteMany({});
});
afterAll(async () => {
await prisma.$disconnect();
await app.close();
});
describe('/version (POST)', () => {
const validDto = {
platform: 'android',
versionCode: 100,
versionName: '1.0.0',
buildNumber: '1',
downloadUrl: 'https://example.com/app.apk',
fileSize: 52428800,
fileSha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
changelog: 'Initial release',
isEnabled: true,
isForceUpdate: false,
createdBy: 'admin',
};
it('应该成功创建新版本', () => {
return request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send(validDto)
.expect(201)
.expect((res) => {
expect(res.body.id).toBeDefined();
expect(res.body.versionCode).toBe(100);
expect(res.body.platform).toBe('android');
});
});
it('应该拒绝无效的版本号', () => {
return request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send({ ...validDto, versionCode: -1 })
.expect(400);
});
it('应该拒绝无效的 SHA256', () => {
return request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send({ ...validDto, fileSha256: 'invalid' })
.expect(400);
});
it('应该拒绝重复的版本号', async () => {
await request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send(validDto)
.expect(201);
return request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send(validDto)
.expect(409);
});
});
describe('/version/check (GET)', () => {
beforeEach(async () => {
await request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send({
platform: 'android',
versionCode: 100,
versionName: '1.0.0',
// ... 其他字段
});
await request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send({
platform: 'android',
versionCode: 101,
versionName: '1.0.1',
// ... 其他字段
});
});
it('应该检测到有更新', () => {
return request(app.getHttpServer())
.get(`/${apiPrefix}/version/check?platform=android&versionCode=99`)
.expect(200)
.expect((res) => {
expect(res.body.hasUpdate).toBe(true);
expect(res.body.latestVersion).toBe('1.0.1');
});
});
it('应该返回无更新', () => {
return request(app.getHttpServer())
.get(`/${apiPrefix}/version/check?platform=android&versionCode=101`)
.expect(200)
.expect((res) => {
expect(res.body.hasUpdate).toBe(false);
});
});
it('应该拒绝缺少参数的请求', () => {
return request(app.getHttpServer())
.get(`/${apiPrefix}/version/check?platform=android`)
.expect(400);
});
});
describe('/version/:id/enable (PATCH)', () => {
let versionId: string;
beforeEach(async () => {
const response = await request(app.getHttpServer())
.post(`/${apiPrefix}/version`)
.send({ /* ... */ isEnabled: false });
versionId = response.body.id;
});
it('应该成功启用版本', () => {
return request(app.getHttpServer())
.patch(`/${apiPrefix}/version/${versionId}/enable`)
.send({ updatedBy: 'admin' })
.expect(204);
});
it('应该拒绝无效的版本 ID', () => {
return request(app.getHttpServer())
.patch(`/${apiPrefix}/version/invalid-id/enable`)
.send({ updatedBy: 'admin' })
.expect(404);
});
});
});
```
### 5.3 运行 E2E 测试
```bash
# 运行所有 E2E 测试
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:e2e
# 调试模式
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:e2e -- --detectOpenHandles
# 单个测试套件
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:e2e -- --testNamePattern="version check"
```
---
## 6. 测试执行
### 6.1 Make 命令
```bash
# 查看所有可用命令
make help
# 单元测试
make test-unit
# 集成测试
make test-integration
# E2E 测试
make test-e2e
# 所有测试
make test-all
# 覆盖率报告
make test-coverage
# Docker 测试
make test-docker
```
### 6.2 npm 脚本
```bash
# 所有测试
npm test
# 单元测试
npm run test:unit
# 集成测试
npm run test:integration
# E2E 测试
npm run test:e2e
# 覆盖率
npm run test:cov
# 监听模式
npm run test:watch
# 调试模式
npm run test:debug
```
### 6.3 自动化脚本
#### 完整测试流程
```bash
#!/bin/bash
# scripts/test-all.sh
set -e
echo "=== Admin Service Complete Test Suite ==="
# 1. 启动数据库
echo "Starting test database..."
docker run -d --name admin-test-db --rm \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=admin_service_test \
-p 5433:5432 \
postgres:16-alpine
sleep 5
# 2. 运行迁移
echo "Running migrations..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npx prisma migrate deploy
# 3. 单元测试
echo "Running unit tests..."
npm run test:unit
# 4. 集成测试
echo "Running integration tests..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:integration
# 5. E2E 测试
echo "Running E2E tests..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:e2e
# 6. 覆盖率报告
echo "Generating coverage report..."
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:cov
# 7. 清理
echo "Cleaning up..."
docker stop admin-test-db
echo "=== All tests passed! ==="
```
---
## 7. 测试覆盖率
### 7.1 覆盖率目标
| 层级 | 覆盖率目标 | 当前状态 |
|-----|-----------|---------|
| **值对象** | 100% | ✅ 已达成 (4/4) |
| **实体** | 95%+ | ✅ 已达成 (1/1) |
| **映射器** | 100% | ✅ 已达成 (1/1) |
| **Repository** | 90%+ | ✅ 已达成 (1/1) |
| **Handler** | 90%+ | ⚠️ 部分 (1/2) |
| **Controller** | 85%+ | ✅ 已达成 (1/1) |
| **整体** | 85%+ | 🎯 目标 |
### 7.2 生成覆盖率报告
```bash
# 生成 HTML 报告
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:cov
# 报告位置
# coverage/lcov-report/index.html
```
### 7.3 覆盖率配置
**package.json**:
```json
{
"jest": {
"collectCoverageFrom": [
"src/**/*.(t|j)s",
"!src/**/*.module.ts",
"!src/**/*.interface.ts",
"!src/main.ts",
"!src/**/*.dto.ts"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 85,
"lines": 85,
"statements": 85
}
}
}
}
```
---
## 8. CI/CD 集成
### 8.1 GitHub Actions 示例
**`.github/workflows/test.yml`**:
```yaml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: admin_service_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/services/admin-service/package-lock.json
- name: Install dependencies
working-directory: backend/services/admin-service
run: npm ci
- name: Run Prisma migrations
working-directory: backend/services/admin-service
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/admin_service_test
run: npx prisma migrate deploy
- name: Generate Prisma client
working-directory: backend/services/admin-service
run: npm run prisma:generate
- name: Run unit tests
working-directory: backend/services/admin-service
run: npm run test:unit
- name: Run integration tests
working-directory: backend/services/admin-service
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/admin_service_test
run: npm run test:integration
- name: Run E2E tests
working-directory: backend/services/admin-service
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/admin_service_test
run: npm run test:e2e
- name: Generate coverage report
working-directory: backend/services/admin-service
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/admin_service_test
run: npm run test:cov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: backend/services/admin-service/coverage/lcov.info
flags: admin-service
```
### 8.2 Pre-commit Hook
**`.husky/pre-commit`**:
```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd backend/services/admin-service
# 运行单元测试
npm run test:unit
# 运行 lint
npm run lint
# 运行格式检查
npm run format:check
```
---
## 9. 最佳实践
### 9.1 测试命名规范
```typescript
// ✅ 推荐: 清晰描述测试意图
it('应该拒绝负数版本号', () => {});
it('应该在禁用版本时自动取消强制更新', () => {});
// ❌ 避免: 模糊的测试名称
it('test version code', () => {});
it('should work', () => {});
```
### 9.2 测试隔离
```typescript
// ✅ 推荐: 每个测试独立准备数据
beforeEach(async () => {
await prisma.appVersion.deleteMany({});
});
it('test 1', async () => {
const version = await createTestData();
// 测试逻辑
});
it('test 2', async () => {
const version = await createTestData(); // 重新创建
// 测试逻辑
});
// ❌ 避免: 测试间共享状态
let sharedVersion;
beforeAll(async () => {
sharedVersion = await createTestData(); // 多个测试共享
});
```
### 9.3 测试数据工厂
```typescript
// 创建测试工厂函数
export function createTestVersionParams(overrides = {}) {
return {
platform: Platform.ANDROID,
versionCode: 100,
versionName: '1.0.0',
buildNumber: '1',
downloadUrl: 'https://example.com/app.apk',
fileSize: 52428800n,
fileSha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
changelog: 'Test version',
isEnabled: true,
isForceUpdate: false,
createdBy: 'test',
...overrides,
};
}
// 使用
const version1 = AppVersion.create(createTestVersionParams());
const version2 = AppVersion.create(createTestVersionParams({ versionCode: 101 }));
```
---
**最后更新**: 2025-12-03
**版本**: 1.0.0
**维护者**: RWA Durian Team