1197 lines
31 KiB
Markdown
1197 lines
31 KiB
Markdown
# 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
|