# Authorization Service 测试文档 ## 目录 1. [测试架构概述](#测试架构概述) 2. [测试金字塔](#测试金字塔) 3. [单元测试](#单元测试) 4. [集成测试](#集成测试) 5. [端到端测试](#端到端测试) 6. [Docker 测试环境](#docker-测试环境) 7. [测试覆盖率](#测试覆盖率) 8. [CI/CD 集成](#cicd-集成) 9. [手动测试指南](#手动测试指南) --- ## 测试架构概述 Authorization Service 采用多层测试策略,确保代码质量和系统稳定性。 ### 测试框架 | 框架 | 用途 | |------|------| | Jest | 测试运行器和断言库 | | @nestjs/testing | NestJS 测试工具 | | supertest | HTTP 请求测试 | | ts-jest | TypeScript 支持 | ### 测试类型 ``` ┌─────────────────────────────────────────────┐ │ E2E Tests │ ← 少量,验证完整流程 │ (test/*.e2e-spec.ts) │ ├─────────────────────────────────────────────┤ │ Integration Tests │ ← 中等,验证模块协作 │ (test/*.integration-spec.ts) │ ├─────────────────────────────────────────────┤ │ Unit Tests │ ← 大量,验证单个组件 │ (src/**/*.spec.ts) │ └─────────────────────────────────────────────┘ ``` --- ## 测试金字塔 ### 测试分布 | 层级 | 数量 | 执行时间 | 覆盖范围 | |------|------|----------|----------| | 单元测试 | 33+ | ~15秒 | 领域层核心逻辑 | | 集成测试 | 30+ | ~25秒 | 领域服务、值对象交互 | | E2E测试 | 6+ | ~20秒 | API端点、认证流程 | ### 运行所有测试 ```bash # 运行所有测试 npm run test:all # 或分别运行 npm run test:unit # 单元测试 npm run test:integration # 集成测试 npm run test:e2e # E2E 测试 ``` --- ## 单元测试 ### 测试位置 单元测试与源代码放在一起: ``` src/ ├── domain/ │ ├── aggregates/ │ │ ├── authorization-role.aggregate.ts │ │ └── authorization-role.aggregate.spec.ts ← 单元测试 │ ├── entities/ │ │ ├── ladder-target-rule.entity.ts │ │ └── ladder-target-rule.entity.spec.ts │ └── value-objects/ │ ├── month.vo.ts │ └── month.vo.spec.ts ``` ### 测试配置 ```json // jest.config.js 或 package.json { "jest": { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "moduleNameMapper": { "^@/(.*)$": "/$1" }, "testEnvironment": "node" } } ``` ### 聚合根测试示例 ```typescript // src/domain/aggregates/authorization-role.aggregate.spec.ts import { AuthorizationRole } from './authorization-role.aggregate' import { UserId, AdminUserId } from '@/domain/value-objects' import { RoleType, AuthorizationStatus } from '@/domain/enums' import { DomainError } from '@/shared/exceptions' describe('AuthorizationRole Aggregate', () => { describe('createAuthProvinceCompany', () => { it('should create a pending province company authorization', () => { const userId = UserId.create('user-123') const auth = AuthorizationRole.createAuthProvinceCompany({ userId, provinceCode: '430000', provinceName: '湖南省', }) expect(auth.userId.value).toBe('user-123') expect(auth.roleType).toBe(RoleType.AUTH_PROVINCE_COMPANY) expect(auth.status).toBe(AuthorizationStatus.PENDING) expect(auth.provinceCode).toBe('430000') expect(auth.provinceName).toBe('湖南省') }) it('should emit AuthorizationAppliedEvent', () => { const userId = UserId.create('user-123') const auth = AuthorizationRole.createAuthProvinceCompany({ userId, provinceCode: '430000', provinceName: '湖南省', }) expect(auth.domainEvents).toHaveLength(1) expect(auth.domainEvents[0].eventName).toBe('authorization.applied') }) }) describe('authorize', () => { it('should change status to APPROVED when pending', () => { const auth = createPendingAuthorization() const adminId = AdminUserId.create('admin-001') auth.authorize(adminId) expect(auth.status).toBe(AuthorizationStatus.APPROVED) expect(auth.authorizedBy?.value).toBe('admin-001') }) it('should throw error when already approved', () => { const auth = createApprovedAuthorization() const adminId = AdminUserId.create('admin-001') expect(() => auth.authorize(adminId)).toThrow(DomainError) }) }) describe('activate', () => { it('should change status to ACTIVE when approved', () => { const auth = createApprovedAuthorization() auth.activate() expect(auth.status).toBe(AuthorizationStatus.ACTIVE) expect(auth.activatedAt).toBeDefined() }) }) describe('revoke', () => { it('should change status to REVOKED with reason', () => { const auth = createActiveAuthorization() const adminId = AdminUserId.create('admin-001') auth.revoke(adminId, '违规操作') expect(auth.status).toBe(AuthorizationStatus.REVOKED) expect(auth.revokedBy?.value).toBe('admin-001') expect(auth.revokeReason).toBe('违规操作') }) }) }) // 测试辅助函数 function createPendingAuthorization(): AuthorizationRole { return AuthorizationRole.createAuthProvinceCompany({ userId: UserId.create('user-123'), provinceCode: '430000', provinceName: '湖南省', }) } function createApprovedAuthorization(): AuthorizationRole { const auth = createPendingAuthorization() auth.authorize(AdminUserId.create('admin-001')) return auth } function createActiveAuthorization(): AuthorizationRole { const auth = createApprovedAuthorization() auth.activate() return auth } ``` ### 值对象测试示例 ```typescript // src/domain/value-objects/month.vo.spec.ts import { Month } from './month.vo' describe('Month Value Object', () => { describe('create', () => { it('should create month from valid string', () => { const month = Month.create('2024-03') expect(month.value).toBe('2024-03') }) it('should throw error for invalid format', () => { expect(() => Month.create('2024/03')).toThrow() expect(() => Month.create('03-2024')).toThrow() expect(() => Month.create('invalid')).toThrow() }) }) describe('current', () => { it('should create current month', () => { const month = Month.current() const now = new Date() const expected = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` expect(month.value).toBe(expected) }) }) describe('navigation', () => { it('should get next month', () => { expect(Month.create('2024-03').next().value).toBe('2024-04') expect(Month.create('2024-12').next().value).toBe('2025-01') }) it('should get previous month', () => { expect(Month.create('2024-03').previous().value).toBe('2024-02') expect(Month.create('2024-01').previous().value).toBe('2023-12') }) }) describe('comparison', () => { it('should compare months correctly', () => { const m1 = Month.create('2024-03') const m2 = Month.create('2024-06') expect(m1.isBefore(m2)).toBe(true) expect(m2.isAfter(m1)).toBe(true) expect(m1.equals(Month.create('2024-03'))).toBe(true) }) }) }) ``` ### 实体测试示例 ```typescript // src/domain/entities/ladder-target-rule.entity.spec.ts import { LadderTargetRule } from './ladder-target-rule.entity' import { RoleType } from '@/domain/enums' describe('LadderTargetRule Entity', () => { describe('getTarget', () => { describe('province company', () => { it.each([ [1, 150, 150], [2, 300, 450], [3, 600, 1050], [5, 2400, 4650], [9, 11750, 50000], [12, 11750, 50000], // 超过9月使用第9月目标 ])('month %i should have monthly=%i, cumulative=%i', (month, monthly, cumulative) => { const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, month) expect(target.monthlyTarget).toBe(monthly) expect(target.cumulativeTarget).toBe(cumulative) }) }) describe('city company', () => { it('should return correct targets', () => { const target = LadderTargetRule.getTarget(RoleType.AUTH_CITY_COMPANY, 1) expect(target.monthlyTarget).toBe(30) expect(target.cumulativeTarget).toBe(30) }) }) describe('community', () => { it('should return fixed target', () => { const target = LadderTargetRule.getTarget(RoleType.COMMUNITY, 1) expect(target.monthlyTarget).toBe(10) expect(target.cumulativeTarget).toBe(10) }) }) }) describe('getFinalTarget', () => { it('should return 50000 for province', () => { expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_PROVINCE_COMPANY)).toBe(50000) }) it('should return 10000 for city', () => { expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_CITY_COMPANY)).toBe(10000) }) }) }) ``` --- ## 集成测试 ### 测试位置 集成测试位于 `test/` 目录: ``` test/ ├── jest-integration.json # 集成测试配置 ├── setup-integration.ts # 测试环境设置 └── domain-services.integration-spec.ts # 领域服务集成测试 ``` ### 测试配置 ```json // test/jest-integration.json { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testRegex": ".integration-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "moduleNameMapper": { "^@/(.*)$": "/../src/$1" }, "setupFilesAfterEnv": ["/setup-integration.ts"], "testTimeout": 30000 } ``` ### 领域服务集成测试 ```typescript // test/domain-services.integration-spec.ts import { Test, TestingModule } from '@nestjs/testing' import { AuthorizationValidatorService, IReferralRepository } from '@/domain/services/authorization-validator.service' import { AssessmentCalculatorService, ITeamStatisticsRepository } from '@/domain/services/assessment-calculator.service' import { IAuthorizationRoleRepository } from '@/domain/repositories' import { UserId, RegionCode, Month } from '@/domain/value-objects' import { RoleType } from '@/domain/enums' import { AuthorizationRole } from '@/domain/aggregates' describe('Domain Services Integration Tests', () => { let module: TestingModule let validatorService: AuthorizationValidatorService let calculatorService: AssessmentCalculatorService // Mock repositories const mockAuthorizationRoleRepository: jest.Mocked = { save: jest.fn(), findById: jest.fn(), findByUserIdAndRoleType: jest.fn(), findByUserIdRoleTypeAndRegion: jest.fn(), findByUserId: jest.fn(), findActiveByRoleTypeAndRegion: jest.fn(), findAllActive: jest.fn(), findPendingByUserId: jest.fn(), findByStatus: jest.fn(), delete: jest.fn(), } const mockReferralRepository: jest.Mocked = { findByUserId: jest.fn(), getAllAncestors: jest.fn(), getAllDescendants: jest.fn(), } const mockTeamStatisticsRepository: jest.Mocked = { findByUserId: jest.fn(), } beforeAll(async () => { module = await Test.createTestingModule({ providers: [ AuthorizationValidatorService, AssessmentCalculatorService, { provide: 'AUTHORIZATION_ROLE_REPOSITORY', useValue: mockAuthorizationRoleRepository, }, { provide: 'REFERRAL_REPOSITORY', useValue: mockReferralRepository, }, { provide: 'TEAM_STATISTICS_REPOSITORY', useValue: mockTeamStatisticsRepository, }, ], }).compile() validatorService = module.get(AuthorizationValidatorService) calculatorService = module.get(AssessmentCalculatorService) }) afterAll(async () => { await module.close() }) beforeEach(() => { jest.clearAllMocks() }) describe('AuthorizationValidatorService', () => { describe('validateAuthorizationRequest', () => { it('should return success when no conflicts', async () => { const userId = UserId.create('user-123') const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) mockReferralRepository.findByUserId.mockResolvedValue(null) const result = await validatorService.validateAuthorizationRequest( userId, roleType, regionCode, mockReferralRepository, mockAuthorizationRoleRepository, ) expect(result.isValid).toBe(true) }) it('should return failure when user already has authorization', async () => { const userId = UserId.create('user-123') const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') const existingAuth = AuthorizationRole.createAuthProvinceCompany({ userId, provinceCode: '440000', provinceName: '广东省', }) mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(existingAuth) const result = await validatorService.validateAuthorizationRequest( userId, roleType, regionCode, mockReferralRepository, mockAuthorizationRoleRepository, ) expect(result.isValid).toBe(false) expect(result.errorMessage).toContain('只能申请一个省代或市代授权') }) it('should return failure when team member has same region authorization', async () => { const userId = UserId.create('user-123') const ancestorUserId = UserId.create('ancestor-user') const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' }) mockReferralRepository.getAllAncestors.mockResolvedValue([ancestorUserId]) mockReferralRepository.getAllDescendants.mockResolvedValue([]) const existingAuth = AuthorizationRole.createAuthProvinceCompany({ userId: ancestorUserId, provinceCode: '430000', provinceName: '湖南省', }) mockAuthorizationRoleRepository.findByUserIdRoleTypeAndRegion.mockResolvedValue(existingAuth) const result = await validatorService.validateAuthorizationRequest( userId, roleType, regionCode, mockReferralRepository, mockAuthorizationRoleRepository, ) expect(result.isValid).toBe(false) expect(result.errorMessage).toContain('本团队已有人申请') }) }) }) describe('AssessmentCalculatorService', () => { describe('assessAndRankRegion', () => { it('should calculate assessments and rank by exceed ratio', async () => { const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') const assessmentMonth = Month.current() // Setup: Two authorizations const auth1 = AuthorizationRole.createAuthProvinceCompany({ userId: UserId.create('user-1'), provinceCode: '430000', provinceName: '湖南省', }) auth1.authorize(AdminUserId.create('admin')) const auth2 = AuthorizationRole.createAuthProvinceCompany({ userId: UserId.create('user-2'), provinceCode: '430000', provinceName: '湖南省', }) auth2.authorize(AdminUserId.create('admin')) mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2]) // User 1 has better stats mockTeamStatisticsRepository.findByUserId .mockResolvedValueOnce({ userId: 'user-1', totalTeamPlantingCount: 200, getProvinceTeamCount: () => 70, }) .mockResolvedValueOnce({ userId: 'user-2', totalTeamPlantingCount: 100, getProvinceTeamCount: () => 35, }) const assessments = await calculatorService.assessAndRankRegion( roleType, regionCode, assessmentMonth, mockAuthorizationRoleRepository, mockTeamStatisticsRepository, ) expect(assessments.length).toBe(2) expect(assessments[0].userId.value).toBe('user-1') expect(assessments[0].rankingInRegion).toBe(1) expect(assessments[0].isFirstPlace).toBe(true) }) }) }) }) ``` --- ## 端到端测试 ### 测试位置 ``` test/ ├── jest-e2e.json # E2E 测试配置 └── app.e2e-spec.ts # E2E 测试 ``` ### E2E 测试配置 ```json // test/jest-e2e.json { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "moduleNameMapper": { "^@/(.*)$": "/../src/$1" }, "testTimeout": 60000 } ``` ### E2E 测试示例 ```typescript // test/app.e2e-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 { GlobalExceptionFilter } from '@/shared/filters' import { TransformInterceptor } from '@/shared/interceptors' import { PrismaClient } from '@prisma/client' describe('Authorization Service E2E Tests', () => { let app: INestApplication let prisma: PrismaClient const isDockerEnv = process.env.NODE_ENV === 'test' && process.env.DATABASE_URL?.includes('test-db') beforeAll(async () => { try { // Initialize Prisma prisma = new PrismaClient() await prisma.$connect() const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile() app = moduleFixture.createNestApplication() app.setGlobalPrefix('api/v1') app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ) app.useGlobalFilters(new GlobalExceptionFilter()) app.useGlobalInterceptors(new TransformInterceptor()) await app.init() console.log('E2E tests initialized successfully') } catch (error) { if (isDockerEnv) { throw new Error(`E2E tests failed in Docker: ${error}`) } console.log('E2E tests skipped: Infrastructure not available') } }, 60000) afterAll(async () => { if (app) await app.close() if (prisma) await prisma.$disconnect() }) describe('Authentication Tests', () => { it('should return 401 for unauthorized access to /api/v1/authorizations/my', async () => { if (!app) return return request(app.getHttpServer()) .get('/api/v1/authorizations/my') .expect(401) }) it('should return 401 for unauthorized admin access', async () => { if (!app) return return request(app.getHttpServer()) .post('/api/v1/admin/authorizations/province-company') .send({ userId: 'test', provinceCode: '430000', provinceName: '湖南省' }) .expect(401) }) }) describe('Validation Tests', () => { it('should return 401 for community authorization without token', async () => { if (!app) return return request(app.getHttpServer()) .post('/api/v1/authorizations/community') .send({ communityName: '测试社区' }) .expect(401) }) it('should return 401 for province authorization without token', async () => { if (!app) return return request(app.getHttpServer()) .post('/api/v1/authorizations/province') .send({ provinceCode: '430000', provinceName: '湖南省' }) .expect(401) }) it('should return 401 for city authorization without token', async () => { if (!app) return return request(app.getHttpServer()) .post('/api/v1/authorizations/city') .send({ cityCode: '430100', cityName: '长沙市' }) .expect(401) }) }) }) ``` --- ## Docker 测试环境 ### Docker Compose 配置 ```yaml # docker-compose.test.yml services: test-db: image: postgres:15-alpine environment: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: authorization_test ports: - "5433:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U test -d authorization_test"] interval: 5s timeout: 5s retries: 5 test-redis: image: redis:7-alpine ports: - "6380:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 5 test-kafka: image: apache/kafka:3.7.0 environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://test-kafka:9092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: 1@test-kafka:9093 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk ports: - "9093:9092" healthcheck: test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] interval: 10s timeout: 10s retries: 10 start_period: 30s authorization-service-test: build: context: . dockerfile: Dockerfile.test environment: DATABASE_URL: postgresql://test:test@test-db:5432/authorization_test REDIS_HOST: test-redis REDIS_PORT: 6379 KAFKA_BROKERS: test-kafka:9092 JWT_SECRET: test-jwt-secret-key JWT_EXPIRES_IN: 1h NODE_ENV: test depends_on: test-db: condition: service_healthy test-redis: condition: service_healthy test-kafka: condition: service_healthy volumes: - ./coverage:/app/coverage command: sh -c "npx prisma migrate deploy && npm run test:unit && npm run test:integration && npm run test:e2e" networks: default: driver: bridge ``` ### 测试 Dockerfile ```dockerfile # Dockerfile.test FROM node:20-alpine WORKDIR /app # Install OpenSSL for Prisma RUN apk add --no-cache openssl openssl-dev libc6-compat # Copy package files COPY package*.json ./ COPY prisma ./prisma/ # Install dependencies RUN npm ci # Generate Prisma client RUN npx prisma generate # Copy source code and tests COPY . . # Default command CMD ["npm", "run", "test:all"] ``` ### 运行 Docker 测试 ```bash # 使用 Makefile make test-docker-all # 或直接使用 docker compose docker compose -f docker-compose.test.yml up --build --abort-on-container-exit ``` --- ## 测试覆盖率 ### 生成覆盖率报告 ```bash # 运行测试并生成覆盖率 npm run test:cov # 覆盖率报告位置 coverage/ ├── lcov-report/ # HTML 报告 │ └── index.html ├── lcov.info # LCOV 格式 └── coverage-summary.json ``` ### 覆盖率目标 | 指标 | 目标 | 说明 | |------|------|------| | 行覆盖率 | ≥80% | 代码行被执行的比例 | | 分支覆盖率 | ≥70% | 条件分支被覆盖的比例 | | 函数覆盖率 | ≥80% | 函数被调用的比例 | --- ## CI/CD 集成 ### GitHub Actions 示例 ```yaml # .github/workflows/test.yml name: Test on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:15-alpine env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: authorization_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis:7-alpine ports: - 6379:6379 steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Generate Prisma Client run: npx prisma generate - name: Run migrations run: npx prisma migrate deploy env: DATABASE_URL: postgresql://test:test@localhost:5432/authorization_test - name: Run unit tests run: npm run test:unit - name: Run integration tests run: npm run test:integration env: DATABASE_URL: postgresql://test:test@localhost:5432/authorization_test REDIS_HOST: localhost REDIS_PORT: 6379 - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info ``` --- ## 手动测试指南 ### 使用 curl 测试 API ```bash # 获取 JWT Token(假设从 Identity Service) TOKEN="eyJhbGciOiJIUzI1NiIs..." # 获取我的授权 curl -X GET http://localhost:3002/api/v1/authorizations/my \ -H "Authorization: Bearer $TOKEN" # 申请省代授权 curl -X POST http://localhost:3002/api/v1/authorizations/province \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "provinceCode": "430000", "provinceName": "湖南省" }' # 管理员审核授权 curl -X POST http://localhost:3002/api/v1/admin/authorizations/auth-123/review \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "approved": true, "reason": "审核通过" }' ``` ### 使用 REST Client (VSCode) 创建 `.http` 文件: ```http ### Variables @baseUrl = http://localhost:3002/api/v1 @token = your-jwt-token ### Get my authorizations GET {{baseUrl}}/authorizations/my Authorization: Bearer {{token}} ### Apply for province authorization POST {{baseUrl}}/authorizations/province Authorization: Bearer {{token}} Content-Type: application/json { "provinceCode": "430000", "provinceName": "湖南省" } ### Admin: Review authorization POST {{baseUrl}}/admin/authorizations/auth-123/review Authorization: Bearer {{adminToken}} Content-Type: application/json { "approved": true, "reason": "审核通过" } ``` --- ## 测试最佳实践 ### 1. 遵循 AAA 模式 ```typescript it('should do something', () => { // Arrange - 准备测试数据 const input = createTestInput() // Act - 执行被测试的操作 const result = service.doSomething(input) // Assert - 验证结果 expect(result).toBe(expected) }) ``` ### 2. 使用描述性测试名称 ```typescript // Good it('should return failure when user already has province authorization') // Bad it('test validation') ``` ### 3. 隔离测试 - 每个测试应该独立运行 - 使用 `beforeEach` 重置状态 - 避免测试之间的依赖 ### 4. Mock 外部依赖 ```typescript const mockRepository: jest.Mocked = { find: jest.fn(), save: jest.fn(), } ``` ### 5. 测试边界条件 - 空值处理 - 边界值 - 异常情况