30 KiB
30 KiB
Admin Service 测试文档
目录
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 测试原则
- 测试金字塔: 单元测试多、集成测试适中、E2E 测试少
- 独立性: 每个测试用例独立运行,不依赖其他测试
- 可重复性: 测试结果稳定,多次运行结果一致
- 快速反馈: 单元测试秒级完成,集成测试分钟级
- 有意义: 测试业务逻辑,而非框架行为
2. 测试环境设置
2.1 本地开发环境
安装依赖
cd backend/services/admin-service
npm install
配置测试环境变量
创建 .env.test:
NODE_ENV=test
APP_PORT=3005
API_PREFIX=api/v1
DATABASE_URL=postgresql://postgres:password@localhost:5433/admin_service_test?schema=public
启动测试数据库
方案 1: Docker
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
psql -U postgres -c "CREATE DATABASE admin_service_test;"
运行数据库迁移
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npx prisma migrate deploy
2.2 WSL2 环境 (推荐)
一键启动脚本
# 使用自动化脚本
chmod +x scripts/test-with-docker-db.sh
./scripts/test-with-docker-db.sh
脚本内容 (scripts/test-with-docker-db.sh):
#!/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
# 启动测试环境
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:
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
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
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 运行单元测试
# 运行所有单元测试
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
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
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 运行集成测试
# 运行所有集成测试
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
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 测试
# 运行所有 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 命令
# 查看所有可用命令
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 脚本
# 所有测试
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 自动化脚本
完整测试流程
#!/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 生成覆盖率报告
# 生成 HTML 报告
DATABASE_URL="postgresql://postgres:password@localhost:5433/admin_service_test" \
npm run test:cov
# 报告位置
# coverage/lcov-report/index.html
7.3 覆盖率配置
package.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:
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:
#!/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 测试命名规范
// ✅ 推荐: 清晰描述测试意图
it('应该拒绝负数版本号', () => {});
it('应该在禁用版本时自动取消强制更新', () => {});
// ❌ 避免: 模糊的测试名称
it('test version code', () => {});
it('should work', () => {});
9.2 测试隔离
// ✅ 推荐: 每个测试独立准备数据
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 测试数据工厂
// 创建测试工厂函数
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