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

30 KiB
Raw Blame History

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 测试原则

  1. 测试金字塔: 单元测试多、集成测试适中、E2E 测试少
  2. 独立性: 每个测试用例独立运行,不依赖其他测试
  3. 可重复性: 测试结果稳定,多次运行结果一致
  4. 快速反馈: 单元测试秒级完成,集成测试分钟级
  5. 有意义: 测试业务逻辑,而非框架行为

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