# 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); prisma = module.get(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); repository = module.get(APP_VERSION_REPOSITORY); prisma = module.get(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); }); 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