29 KiB
29 KiB
Authorization Service 测试文档
目录
测试架构概述
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端点、认证流程 |
运行所有测试
# 运行所有测试
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
测试配置
// jest.config.js 或 package.json
{
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/$1"
},
"testEnvironment": "node"
}
}
聚合根测试示例
// 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
}
值对象测试示例
// 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)
})
})
})
实体测试示例
// 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 # 领域服务集成测试
测试配置
// test/jest-integration.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".integration-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/../src/$1"
},
"setupFilesAfterEnv": ["<rootDir>/setup-integration.ts"],
"testTimeout": 30000
}
领域服务集成测试
// 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<IAuthorizationRoleRepository> = {
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<IReferralRepository> = {
findByUserId: jest.fn(),
getAllAncestors: jest.fn(),
getAllDescendants: jest.fn(),
}
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
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 测试配置
// test/jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/../src/$1"
},
"testTimeout": 60000
}
E2E 测试示例
// 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 配置
# 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.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 测试
# 使用 Makefile
make test-docker-all
# 或直接使用 docker compose
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
测试覆盖率
生成覆盖率报告
# 运行测试并生成覆盖率
npm run test:cov
# 覆盖率报告位置
coverage/
├── lcov-report/ # HTML 报告
│ └── index.html
├── lcov.info # LCOV 格式
└── coverage-summary.json
覆盖率目标
| 指标 | 目标 | 说明 |
|---|---|---|
| 行覆盖率 | ≥80% | 代码行被执行的比例 |
| 分支覆盖率 | ≥70% | 条件分支被覆盖的比例 |
| 函数覆盖率 | ≥80% | 函数被调用的比例 |
CI/CD 集成
GitHub Actions 示例
# .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
# 获取 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 文件:
### 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 模式
it('should do something', () => {
// Arrange - 准备测试数据
const input = createTestInput()
// Act - 执行被测试的操作
const result = service.doSomething(input)
// Assert - 验证结果
expect(result).toBe(expected)
})
2. 使用描述性测试名称
// Good
it('should return failure when user already has province authorization')
// Bad
it('test validation')
3. 隔离测试
- 每个测试应该独立运行
- 使用
beforeEach重置状态 - 避免测试之间的依赖
4. Mock 外部依赖
const mockRepository: jest.Mocked<IRepository> = {
find: jest.fn(),
save: jest.fn(),
}
5. 测试边界条件
- 空值处理
- 边界值
- 异常情况