{T}

集成测试

介绍

什么是集成测试

集成测试(Integration Testing)是一种软件测试方法,用于验证多个模块、组件或服务之间的协作是否正常。它介于单元测试和端到端测试之间,关注的是模块间的接口和数据流转。

code
单元测试 → 集成测试 → 端到端测试
  ↓           ↓           ↓
单一模块    多模块协作    完整系统

测试金字塔

code
        /\
       /E2E\        数量少,速度慢,成本高
      /------\
     / 集成测试 \      数量适中,速度适中
    /----------\
   /   单元测试   \    数量多,速度快,成本低
  /--------------\
测试类型测试范围执行速度维护成本数量占比
单元测试单个函数/组件毫秒级70%
集成测试多个模块协作秒级20%
E2E 测试完整业务流程分钟级10%

集成测试的价值

优势

  • 发现接口问题:检测模块间的接口不匹配、数据格式错误
  • 验证数据流转:确保数据在不同模块间正确传递和转换
  • 测试真实场景:比单元测试更接近真实使用场景
  • 发现配置问题:检测环境配置、依赖注入等问题
  • 提高信心:增强对系统整体行为的信心

限制

  • 执行较慢:涉及数据库、网络等外部依赖
  • 维护成本高:需要管理测试环境和数据
  • 调试困难:错误可能来自多个模块
  • 不确定性:可能受外部因素影响

适用场景

适合集成测试的场景

  • ✅ API 接口测试(RESTful、GraphQL)
  • ✅ 数据库操作测试(CRUD、事务)
  • ✅ 微服务间通信测试
  • ✅ 认证授权流程测试
  • ✅ 文件上传下载测试
  • ✅ 第三方服务集成测试

不适合集成测试的场景

  • ❌ 纯函数逻辑(应使用单元测试)
  • ❌ UI 交互流程(应使用 E2E 测试)
  • ❌ 复杂业务规则(应拆分为单元测试)

快速开始

项目初始化

1. 安装依赖

bash
# 核心依赖
pnpm add -D jest @types/jest ts-jest

# HTTP 测试
pnpm add -D supertest @types/supertest

# 数据库测试
pnpm add -D mongodb-memory-server

# 工具库
pnpm add -D faker @faker-js/faker
pnpm add -D dotenv

2. 项目结构

code
project/
├── src/
│   ├── modules/
│   │   ├── user/
│   │   │   ├── user.controller.ts
│   │   │   ├── user.service.ts
│   │   │   └── user.model.ts
│   │   └── auth/
│   ├── app.ts
│   └── server.ts
├── tests/
│   ├── integration/
│   │   ├── user.test.ts
│   │   ├── auth.test.ts
│   │   └── api.test.ts
│   ├── fixtures/
│   │   └── users.fixture.ts
│   ├── factories/
│   │   └── user.factory.ts
│   ├── setup/
│   │   ├── db.ts
│   │   └── app.ts
│   ├── setup.ts
│   ├── globalSetup.ts
│   └── globalTeardown.ts
├── jest.config.js
└── package.json

3. Jest 配置

javascript
// jest.config.js
module.exports = {
  testEnvironment: 'node',
  roots: ['<rootDir>/tests'],
  testMatch: ['**/*.integration.test.ts', '**/integration/**/*.test.ts'],
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
  globalSetup: '<rootDir>/tests/globalSetup.ts',
  globalTeardown: '<rootDir>/tests/globalTeardown.ts',
  testTimeout: 30000, // 集成测试需要更长超时
  verbose: true,
  forceExit: true,
  detectOpenHandles: true
}

第一个集成测试

应用代码

typescript
// src/app.ts
import express from 'express'
import bodyParser from 'body-parser'
import userRouter from './modules/user/user.router'

const app = express()

app.use(bodyParser.json())
app.use('/api/users', userRouter)

app.get('/health', (req, res) => {
  res.json({ status: 'ok' })
})

export default app
typescript
// src/modules/user/user.router.ts
import { Router } from 'express'
import UserController from './user.controller'

const router = Router()
const controller = new UserController()

router.get('/', controller.getAll)
router.get('/:id', controller.getById)
router.post('/', controller.create)
router.put('/:id', controller.update)
router.delete('/:id', controller.delete)

export default router

测试代码

typescript
// tests/integration/user.test.ts
import request from 'supertest'
import app from '../../src/app'
import User from '../../src/modules/user/user.model'
import { setupDatabase, cleanupDatabase } from '../setup/db'

describe('User API Integration Tests', () => {
  // 在所有测试前设置数据库
  beforeAll(async () => {
    await setupDatabase()
  })

  // 在所有测试后清理数据库
  afterAll(async () => {
    await cleanupDatabase()
  })

  // 每个测试后清理数据
  afterEach(async () => {
    await User.deleteMany({})
  })

  describe('GET /api/users', () => {
    test('should return empty array when no users', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect(200)

      expect(response.body).toEqual([])
    })

    test('should return all users', async () => {
      // 准备测试数据
      await User.create([
        { name: 'Alice', email: 'alice@example.com' },
        { name: 'Bob', email: 'bob@example.com' }
      ])

      const response = await request(app)
        .get('/api/users')
        .expect(200)

      expect(response.body).toHaveLength(2)
      expect(response.body[0]).toMatchObject({
        name: 'Alice',
        email: 'alice@example.com'
      })
    })
  })

  describe('POST /api/users', () => {
    test('should create a new user', async () => {
      const userData = {
        name: 'Charlie',
        email: 'charlie@example.com',
        password: 'password123'
      }

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201)

      expect(response.body).toMatchObject({
        name: userData.name,
        email: userData.email
      })

      // 验证数据库中的数据
      const user = await User.findOne({ email: userData.email })
      expect(user).not.toBeNull()
      expect(user.name).toBe(userData.name)
    })

    test('should return 400 for invalid data', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: '' }) // 缺少必需字段
        .expect(400)

      expect(response.body).toHaveProperty('error')
    })

    test('should return 409 for duplicate email', async () => {
      const userData = {
        name: 'Alice',
        email: 'alice@example.com'
      }

      // 创建第一个用户
      await User.create(userData)

      // 尝试创建重复邮箱的用户
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(409)

      expect(response.body.error).toContain('already exists')
    })
  })

  describe('GET /api/users/:id', () => {
    test('should return user by id', async () => {
      const user = await User.create({
        name: 'David',
        email: 'david@example.com'
      })

      const response = await request(app)
        .get(`/api/users/${user._id}`)
        .expect(200)

      expect(response.body.name).toBe('David')
    })

    test('should return 404 for non-existent user', async () => {
      const fakeId = '507f1f77bcf86cd799439011'
      
      await request(app)
        .get(`/api/users/${fakeId}`)
        .expect(404)
    })
  })

  describe('PUT /api/users/:id', () => {
    test('should update user', async () => {
      const user = await User.create({
        name: 'Eve',
        email: 'eve@example.com'
      })

      const updateData = { name: 'Eve Updated' }

      const response = await request(app)
        .put(`/api/users/${user._id}`)
        .send(updateData)
        .expect(200)

      expect(response.body.name).toBe('Eve Updated')

      // 验证数据库更新
      const updatedUser = await User.findById(user._id)
      expect(updatedUser.name).toBe('Eve Updated')
    })
  })

  describe('DELETE /api/users/:id', () => {
    test('should delete user', async () => {
      const user = await User.create({
        name: 'Frank',
        email: 'frank@example.com'
      })

      await request(app)
        .delete(`/api/users/${user._id}`)
        .expect(204)

      // 验证已删除
      const deletedUser = await User.findById(user._id)
      expect(deletedUser).toBeNull()
    })
  })
})

测试环境配置

Jest 配置

完整配置示例

javascript
// jest.config.js
module.exports = {
  // 测试环境
  testEnvironment: 'node',
  
  // 测试文件匹配
  roots: ['<rootDir>/tests'],
  testMatch: [
    '**/*.integration.test.ts',
    '**/integration/**/*.test.ts'
  ],
  
  // 设置文件
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
  globalSetup: '<rootDir>/tests/globalSetup.ts',
  globalTeardown: '<rootDir>/tests/globalTeardown.ts',
  
  // 超时配置
  testTimeout: 30000,
  
  // 覆盖率配置
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  coverageThreshold: {
    global: {
      branches: 50,
      functions: 50,
      lines: 50,
      statements: 50
    }
  },
  
  // TypeScript 支持
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  },
  
  // 模块解析
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@tests/(.*)$': '<rootDir>/tests/$1'
  },
  
  // 其他配置
  verbose: true,
  forceExit: true,
  detectOpenHandles: true,
  clearMocks: true,
  restoreMocks: true
}

环境变量管理

环境变量文件

bash
# .env.test
NODE_ENV=test
PORT=3001

# 数据库配置
DB_HOST=localhost
DB_PORT=27017
DB_NAME=test_db

# JWT 配置
JWT_SECRET=test-secret-key
JWT_EXPIRES_IN=1h

# 外部服务
API_BASE_URL=https://api.test.example.com
STRIPE_API_KEY=sk_test_xxx

加载环境变量

typescript
// tests/setup.ts
import dotenv from 'dotenv'
import path from 'path'

// 加载测试环境变量
dotenv.config({ path: path.resolve(__dirname, '../.env.test') })

// 设置全局超时
jest.setTimeout(30000)

// 全局钩子
beforeAll(async () => {
  console.log('Test environment:', process.env.NODE_ENV)
})

afterAll(async () => {
  console.log('All tests completed')
})

全局设置与清理

全局设置

typescript
// tests/globalSetup.ts
import { MongoMemoryServer } from 'mongodb-memory-server'
import mongoose from 'mongoose'

let mongoServer: MongoMemoryServer

export default async function globalSetup() {
  // 启动内存 MongoDB
  mongoServer = await MongoMemoryServer.create()
  const uri = mongoServer.getUri()
  
  // 设置全局变量供测试使用
  process.env.MONGO_URI = uri
  
  // 连接数据库
  await mongoose.connect(uri)
  
  console.log('Global setup completed')
  console.log('MongoDB URI:', uri)
}

// 导出用于清理
export { mongoServer }

全局清理

typescript
// tests/globalTeardown.ts
import mongoose from 'mongoose'
import { mongoServer } from './globalSetup'

export default async function globalTeardown() {
  // 关闭数据库连接
  await mongoose.disconnect()
  
  // 停止内存数据库
  if (mongoServer) {
    await mongoServer.stop()
  }
  
  console.log('Global teardown completed')
}

数据库集成测试

测试数据库策略

策略对比

策略优点缺点适用场景
内存数据库速度快、隔离性好与真实数据库有差异大多数集成测试
测试数据库实例真实环境需要管理、速度较慢复杂查询测试
Docker 容器环境一致需要额外配置CI/CD 环境
Mock 数据库最快不测试真实数据库逻辑简单场景

推荐策略

code
开发环境 → 内存数据库(mongodb-memory-server)
CI/CD → Docker 容器或内存数据库
关键业务 → 真实测试数据库

使用内存数据库

MongoDB 内存数据库

typescript
// tests/setup/db.ts
import mongoose from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'

let mongoServer: MongoMemoryServer

export async function setupDatabase() {
  mongoServer = await MongoMemoryServer.create()
  const uri = mongoServer.getUri()
  
  await mongoose.connect(uri)
  
  return uri
}

export async function cleanupDatabase() {
  await mongoose.disconnect()
  await mongoServer.stop()
}

export async function clearCollections() {
  const collections = mongoose.connection.collections
  
  for (const key in collections) {
    await collections[key].deleteMany({})
  }
}

MongoDB 集成测试

Model 测试

typescript
// tests/integration/user.model.test.ts
import mongoose from 'mongoose'
import User from '../../src/models/User'
import { setupDatabase, clearCollections } from '../setup/db'

describe('User Model', () => {
  beforeAll(async () => {
    await setupDatabase()
  })

  afterEach(async () => {
    await clearCollections()
  })

  describe('create', () => {
    test('should create user with valid data', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      }

      const user = await User.create(userData)

      expect(user._id).toBeDefined()
      expect(user.name).toBe(userData.name)
      expect(user.email).toBe(userData.email)
      expect(user.password).not.toBe(userData.password) // 应该被加密
      expect(user.createdAt).toBeDefined()
    })

    test('should throw error for duplicate email', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      }

      await User.create(userData)

      await expect(User.create(userData)).rejects.toThrow()
    })

    test('should throw error for invalid email', async () => {
      const userData = {
        name: 'John Doe',
        email: 'invalid-email',
        password: 'password123'
      }

      await expect(User.create(userData)).rejects.toThrow()
    })

    test('should hash password before saving', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      }

      const user = await User.create(userData)
      
      expect(user.password).not.toBe('password123')
      expect(user.password.length).toBeGreaterThan(20)
    })
  })

  describe('methods', () => {
    test('should compare password correctly', async () => {
      const user = await User.create({
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      })

      const isMatch = await user.comparePassword('password123')
      expect(isMatch).toBe(true)

      const isWrongMatch = await user.comparePassword('wrongpassword')
      expect(isWrongMatch).toBe(false)
    })
  })

  describe('statics', () => {
    test('should find user by email', async () => {
      await User.create({
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      })

      const user = await User.findByEmail('john@example.com')
      expect(user).not.toBeNull()
      expect(user.name).toBe('John Doe')
    })
  })
})

复杂查询测试

typescript
// tests/integration/complex-query.test.ts
import Order from '../../src/models/Order'
import Product from '../../src/models/Product'
import User from '../../src/models/User'
import { setupDatabase, clearCollections } from '../setup/db'

describe('Complex Database Queries', () => {
  let user: any
  let products: any[]

  beforeAll(async () => {
    await setupDatabase()
  })

  beforeEach(async () => {
    await clearCollections()

    // 创建测试数据
    user = await User.create({
      name: 'Test User',
      email: 'test@example.com',
      password: 'password123'
    })

    products = await Product.create([
      { name: 'Product 1', price: 100, category: 'A' },
      { name: 'Product 2', price: 200, category: 'B' },
      { name: 'Product 3', price: 150, category: 'A' }
    ])
  })

  describe('Aggregation', () => {
    test('should calculate total order amount', async () => {
      // 创建订单
      await Order.create([
        { user: user._id, product: products[0]._id, quantity: 2 },
        { user: user._id, product: products[1]._id, quantity: 1 }
      ])

      const result = await Order.aggregate([
        { $match: { user: user._id } },
        {
          $lookup: {
            from: 'products',
            localField: 'product',
            foreignField: '_id',
            as: 'productDetails'
          }
        },
        { $unwind: '$productDetails' },
        {
          $group: {
            _id: null,
            totalAmount: {
              $sum: { $multiply: ['$quantity', '$productDetails.price'] }
            }
          }
        }
      ])

      expect(result[0].totalAmount).toBe(400) // (100 * 2) + (200 * 1)
    })

    test('should group orders by category', async () => {
      await Order.create([
        { user: user._id, product: products[0]._id, quantity: 1 },
        { user: user._id, product: products[2]._id, quantity: 2 }
      ])

      const result = await Order.aggregate([
        {
          $lookup: {
            from: 'products',
            localField: 'product',
            foreignField: '_id',
            as: 'product'
          }
        },
        { $unwind: '$product' },
        {
          $group: {
            _id: '$product.category',
            totalQuantity: { $sum: '$quantity' }
          }
        }
      ])

      const categoryA = result.find(r => r._id === 'A')
      expect(categoryA.totalQuantity).toBe(3)
    })
  })

  describe('Transaction', () => {
    test('should rollback on error', async () => {
      const session = await mongoose.startSession()
      session.startTransaction()

      try {
        // 创建订单
        await Order.create([{
          user: user._id,
          product: products[0]._id,
          quantity: 1
        }], { session })

        // 模拟错误
        throw new Error('Simulated error')

        await session.commitTransaction()
      } catch (error) {
        await session.abortTransaction()
      } finally {
        session.endSession()
      }

      // 验证已回滚
      const orders = await Order.find()
      expect(orders).toHaveLength(0)
    })
  })
})

PostgreSQL 集成测试

配置

typescript
// tests/setup/postgres.ts
import { Pool } from 'pg'
import { execSync } from 'child_process'

let pool: Pool

export async function setupPostgres() {
  pool = new Pool({
    host: process.env.PG_HOST || 'localhost',
    port: parseInt(process.env.PG_PORT || '5432'),
    database: process.env.PG_DATABASE || 'test_db',
    user: process.env.PG_USER || 'postgres',
    password: process.env.PG_PASSWORD || 'postgres'
  })

  // 运行迁移
  execSync('npm run migrate:test')

  return pool
}

export async function cleanupPostgres() {
  // 清空所有表
  const client = await pool.connect()
  try {
    await client.query('TRUNCATE ALL TABLES CASCADE')
  } finally {
    client.release()
  }
  await pool.end()
}

export { pool }

测试示例

typescript
// tests/integration/postgres.test.ts
import { setupPostgres, cleanupPostgres, pool } from '../setup/postgres'

describe('PostgreSQL Integration Tests', () => {
  beforeAll(async () => {
    await setupPostgres()
  })

  afterEach(async () => {
    await pool.query('TRUNCATE users, orders RESTART IDENTITY CASCADE')
  })

  afterAll(async () => {
    await cleanupPostgres()
  })

  test('should insert and retrieve user', async () => {
    const result = await pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      ['John Doe', 'john@example.com']
    )

    expect(result.rows[0].name).toBe('John Doe')
    expect(result.rows[0].email).toBe('john@example.com')
  })

  test('should handle foreign key constraint', async () => {
    // 创建用户
    const userResult = await pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id',
      ['John Doe', 'john@example.com']
    )
    const userId = userResult.rows[0].id

    // 创建订单
    const orderResult = await pool.query(
      'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING *',
      [userId, 100]
    )

    expect(orderResult.rows[0].user_id).toBe(userId)
  })
})

Redis 集成测试

配置

typescript
// tests/setup/redis.ts
import Redis from 'ioredis-mock'

export const redis = new Redis()

export async function clearRedis() {
  await redis.flushall()
}

export async function closeRedis() {
  await redis.quit()
}

测试示例

typescript
// tests/integration/cache.test.ts
import { redis, clearRedis, closeRedis } from '../setup/redis'
import CacheService from '../../src/services/CacheService'

describe('Cache Integration Tests', () => {
  const cacheService = new CacheService(redis)

  beforeEach(async () => {
    await clearRedis()
  })

  afterAll(async () => {
    await closeRedis()
  })

  describe('set', () => {
    test('should set value with TTL', async () => {
      await cacheService.set('key1', 'value1', 60)

      const value = await redis.get('key1')
      expect(value).toBe('value1')

      const ttl = await redis.ttl('key1')
      expect(ttl).toBeLessThanOrEqual(60)
      expect(ttl).toBeGreaterThan(0)
    })
  })

  describe('get', () => {
    test('should return null for non-existent key', async () => {
      const value = await cacheService.get('nonexistent')
      expect(value).toBeNull()
    })

    test('should return cached value', async () => {
      await redis.set('key1', 'value1')

      const value = await cacheService.get('key1')
      expect(value).toBe('value1')
    })
  })

  describe('delete', () => {
    test('should delete key', async () => {
      await redis.set('key1', 'value1')
      await cacheService.delete('key1')

      const value = await redis.get('key1')
      expect(value).toBeNull()
    })
  })

  describe('increment', () => {
    test('should increment counter', async () => {
      await cacheService.increment('counter')
      expect(await redis.get('counter')).toBe('1')

      await cacheService.increment('counter')
      expect(await redis.get('counter')).toBe('2')
    })
  })
})

API 集成测试

Express 应用测试

应用初始化

typescript
// tests/setup/app.ts
import express from 'express'
import bodyParser from 'body-parser'
import cors from 'cors'
import helmet from 'helmet'
import userRouter from '../../src/routes/user'
import authRouter from '../../src/routes/auth'

export function createApp() {
  const app = express()

  // 中间件
  app.use(helmet())
  app.use(cors())
  app.use(bodyParser.json())
  app.use(bodyParser.urlencoded({ extended: true }))

  // 路由
  app.use('/api/auth', authRouter)
  app.use('/api/users', userRouter)

  // 错误处理
  app.use((err: any, req: any, res: any, next: any) => {
    console.error(err.stack)
    res.status(500).json({ error: 'Internal server error' })
  })

  return app
}

完整测试示例

typescript
// tests/integration/app.test.ts
import request from 'supertest'
import { createApp } from '../setup/app'
import { setupDatabase, clearCollections } from '../setup/db'

const app = createApp()

describe('Express App Integration', () => {
  beforeAll(async () => {
    await setupDatabase()
  })

  afterEach(async () => {
    await clearCollections()
  })

  describe('Middleware', () => {
    test('should parse JSON body', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: 'John', email: 'john@example.com' })
        .set('Content-Type', 'application/json')

      expect(response.status).not.toBe(400) // 不是解析错误
    })

    test('should set security headers', async () => {
      const response = await request(app)
        .get('/api/users')

      expect(response.headers['x-content-type-options']).toBe('nosniff')
      expect(response.headers['x-frame-options']).toBe('SAMEORIGIN')
    })
  })

  describe('Error handling', () => {
    test('should handle 404', async () => {
      const response = await request(app)
        .get('/nonexistent')
        .expect(404)

      expect(response.body).toHaveProperty('error')
    })

    test('should handle validation errors', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: '' })
        .expect(400)

      expect(response.body).toHaveProperty('errors')
    })
  })
})

RESTful API 测试

CRUD 操作测试

typescript
// tests/integration/crud.test.ts
import request from 'supertest'
import { createApp } from '../setup/app'
import User from '../../src/models/User'
import { setupDatabase, clearCollections } from '../setup/db'

const app = createApp()

describe('RESTful API Tests', () => {
  describe('Create', () => {
    test('POST /api/users - should create user', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      }

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect('Content-Type', /json/)
        .expect(201)

      expect(response.body).toMatchObject({
        name: userData.name,
        email: userData.email
      })
    })

    test('should return 400 for invalid data', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({})
        .expect(400)

      expect(response.body).toHaveProperty('errors')
    })
  })

  describe('Read', () => {
    test('GET /api/users - should list users', async () => {
      // 准备数据
      await User.create([
        { name: 'Alice', email: 'alice@example.com', password: 'pass' },
        { name: 'Bob', email: 'bob@example.com', password: 'pass' }
      ])

      const response = await request(app)
        .get('/api/users')
        .expect(200)

      expect(response.body).toHaveLength(2)
      expect(response.body[0]).toHaveProperty('name')
    })

    test('GET /api/users/:id - should get user by id', async () => {
      const user = await User.create({
        name: 'Alice',
        email: 'alice@example.com',
        password: 'pass'
      })

      const response = await request(app)
        .get(`/api/users/${user._id}`)
        .expect(200)

      expect(response.body.name).toBe('Alice')
    })

    test('should return 404 for non-existent user', async () => {
      const fakeId = '507f1f77bcf86cd799439011'

      await request(app)
        .get(`/api/users/${fakeId}`)
        .expect(404)
    })
  })

  describe('Update', () => {
    test('PUT /api/users/:id - should update user', async () => {
      const user = await User.create({
        name: 'Alice',
        email: 'alice@example.com',
        password: 'pass'
      })

      const response = await request(app)
        .put(`/api/users/${user._id}`)
        .send({ name: 'Alice Updated' })
        .expect(200)

      expect(response.body.name).toBe('Alice Updated')
    })

    test('PATCH /api/users/:id - should partially update user', async () => {
      const user = await User.create({
        name: 'Alice',
        email: 'alice@example.com',
        password: 'pass'
      })

      const response = await request(app)
        .patch(`/api/users/${user._id}`)
        .send({ name: 'Alice Patched' })
        .expect(200)

      expect(response.body.name).toBe('Alice Patched')
      expect(response.body.email).toBe('alice@example.com')
    })
  })

  describe('Delete', () => {
    test('DELETE /api/users/:id - should delete user', async () => {
      const user = await User.create({
        name: 'Alice',
        email: 'alice@example.com',
        password: 'pass'
      })

      await request(app)
        .delete(`/api/users/${user._id}`)
        .expect(204)

      // 验证已删除
      const deletedUser = await User.findById(user._id)
      expect(deletedUser).toBeNull()
    })
  })
})

分页和过滤

typescript
describe('Pagination and Filtering', () => {
  beforeEach(async () => {
    await User.create([
      { name: 'Alice', email: 'alice@example.com', role: 'admin', password: 'pass' },
      { name: 'Bob', email: 'bob@example.com', role: 'user', password: 'pass' },
      { name: 'Charlie', email: 'charlie@example.com', role: 'user', password: 'pass' },
      { name: 'David', email: 'david@example.com', role: 'admin', password: 'pass' }
    ])
  })

  describe('Pagination', () => {
    test('should support page and limit', async () => {
      const response = await request(app)
        .get('/api/users?page=1&limit=2')
        .expect(200)

      expect(response.body.users).toHaveLength(2)
      expect(response.body).toHaveProperty('total', 4)
      expect(response.body).toHaveProperty('page', 1)
      expect(response.body).toHaveProperty('totalPages', 2)
    })

    test('should handle invalid pagination parameters', async () => {
      const response = await request(app)
        .get('/api/users?page=-1&limit=0')
        .expect(400)

      expect(response.body).toHaveProperty('error')
    })
  })

  describe('Filtering', () => {
    test('should filter by role', async () => {
      const response = await request(app)
        .get('/api/users?role=admin')
        .expect(200)

      expect(response.body).toHaveLength(2)
      response.body.forEach(user => {
        expect(user.role).toBe('admin')
      })
    })

    test('should support multiple filters', async () => {
      const response = await request(app)
        .get('/api/users?role=user&name=Alice')
        .expect(200)

      expect(response.body).toHaveLength(0)
    })
  })

  describe('Sorting', () => {
    test('should sort by name ascending', async () => {
      const response = await request(app)
        .get('/api/users?sort=name&order=asc')
        .expect(200)

      expect(response.body[0].name).toBe('Alice')
      expect(response.body[1].name).toBe('Bob')
    })

    test('should sort by name descending', async () => {
      const response = await request(app)
        .get('/api/users?sort=name&order=desc')
        .expect(200)

      expect(response.body[0].name).toBe('David')
    })
  })

  describe('Search', () => {
    test('should search by name', async () => {
      const response = await request(app)
        .get('/api/users?search=alice')
        .expect(200)

      expect(response.body).toHaveLength(1)
      expect(response.body[0].name).toBe('Alice')
    })
  })
})

GraphQL API 测试

测试配置

typescript
// tests/integration/graphql.test.ts
import request from 'supertest'
import { createApp } from '../setup/app'
import User from '../../src/models/User'
import { setupDatabase, clearCollections } from '../setup/db'

const app = createApp()

describe('GraphQL API Tests', () => {
  const graphqlEndpoint = '/graphql'

  describe('Query', () => {
    test('should query users', async () => {
      await User.create([
        { name: 'Alice', email: 'alice@example.com', password: 'pass' },
        { name: 'Bob', email: 'bob@example.com', password: 'pass' }
      ])

      const query = `
        query {
          users {
            id
            name
            email
          }
        }
      `

      const response = await request(app)
        .post(graphqlEndpoint)
        .send({ query })
        .expect(200)

      expect(response.body.data.users).toHaveLength(2)
      expect(response.body.data.users[0]).toHaveProperty('name')
    })

    test('should query single user', async () => {
      const user = await User.create({
        name: 'Alice',
        email: 'alice@example.com',
        password: 'pass'
      })

      const query = `
        query($id: ID!) {
          user(id: $id) {
            id
            name
            email
          }
        }
      `

      const response = await request(app)
        .post(graphqlEndpoint)
        .send({
          query,
          variables: { id: user._id.toString() }
        })
        .expect(200)

      expect(response.body.data.user.name).toBe('Alice')
    })
  })

  describe('Mutation', () => {
    test('should create user', async () => {
      const mutation = `
        mutation($input: CreateUserInput!) {
          createUser(input: $input) {
            id
            name
            email
          }
        }
      `

      const response = await request(app)
        .post(graphqlEndpoint)
        .send({
          query: mutation,
          variables: {
            input: {
              name: 'Alice',
              email: 'alice@example.com',
              password: 'password123'
            }
          }
        })
        .expect(200)

      expect(response.body.data.createUser.name).toBe('Alice')
    })

    test('should update user', async () => {
      const user = await User.create({
        name: 'Alice',
        email: 'alice@example.com',
        password: 'pass'
      })

      const mutation = `
        mutation($id: ID!, $input: UpdateUserInput!) {
          updateUser(id: $id, input: $input) {
            id
            name
          }
        }
      `

      const response = await request(app)
        .post(graphqlEndpoint)
        .send({
          query: mutation,
          variables: {
            id: user._id.toString(),
            input: { name: 'Alice Updated' }
          }
        })
        .expect(200)

      expect(response.body.data.updateUser.name).toBe('Alice Updated')
    })
  })

  describe('Error handling', () => {
    test('should return error for invalid query', async () => {
      const query = `
        query {
          nonexistentField
        }
      `

      const response = await request(app)
        .post(graphqlEndpoint)
        .send({ query })
        .expect(400)

      expect(response.body).toHaveProperty('errors')
    })
  })
})

认证授权测试

JWT 认证测试

typescript
// tests/integration/auth.test.ts
import request from 'supertest'
import { createApp } from '../setup/app'
import User from '../../src/models/User'
import { setupDatabase, clearCollections } from '../setup/db'

const app = createApp()

describe('Authentication Tests', () => {
  let token: string
  let user: any

  beforeEach(async () => {
    await clearCollections()

    user = await User.create({
      name: 'Test User',
      email: 'test@example.com',
      password: 'password123'
    })
    token = user.generateAuthToken()
  })

  describe('Login', () => {
    test('POST /api/auth/login - should login with valid credentials', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'password123'
        })
        .expect(200)

      expect(response.body).toHaveProperty('token')
      expect(response.body.user.email).toBe('test@example.com')
    })

    test('should return 401 for invalid credentials', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'wrongpassword'
        })
        .expect(401)

      expect(response.body).toHaveProperty('error')
    })

    test('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'nonexistent@example.com',
          password: 'password123'
        })
        .expect(404)

      expect(response.body.error).toContain('not found')
    })
  })

  describe('Protected Routes', () => {
    test('should access protected route with valid token', async () => {
      const response = await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${token}`)
        .expect(200)

      expect(response.body.email).toBe('test@example.com')
    })

    test('should return 401 without token', async () => {
      await request(app)
        .get('/api/profile')
        .expect(401)
    })

    test('should return 401 with invalid token', async () => {
      await request(app)
        .get('/api/profile')
        .set('Authorization', 'Bearer invalid-token')
        .expect(401)
    })

    test('should return 401 with expired token', async () => {
      // 创建过期 token
      const expiredToken = user.generateAuthToken('1ms')
      
      // 等待 token 过期
      await new Promise(resolve => setTimeout(resolve, 10))

      await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${expiredToken}`)
        .expect(401)
    })
  })

  describe('Authorization', () => {
    test('should allow admin to access admin route', async () => {
      user.role = 'admin'
      await user.save()
      const adminToken = user.generateAuthToken()

      await request(app)
        .get('/api/admin/dashboard')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200)
    })

    test('should deny regular user from admin route', async () => {
      user.role = 'user'
      await user.save()
      const userToken = user.generateAuthToken()

      await request(app)
        .get('/api/admin/dashboard')
        .set('Authorization', `Bearer ${userToken}`)
        .expect(403)
    })
  })

  describe('Refresh Token', () => {
    test('should refresh token', async () => {
      const refreshToken = user.generateRefreshToken()

      const response = await request(app)
        .post('/api/auth/refresh')
        .send({ refreshToken })
        .expect(200)

      expect(response.body).toHaveProperty('token')
      expect(response.body.token).not.toBe(token)
    })
  })

  describe('Logout', () => {
    test('should logout successfully', async () => {
      const response = await request(app)
        .post('/api/auth/logout')
        .set('Authorization', `Bearer ${token}`)
        .expect(200)

      expect(response.body.message).toContain('Logged out')
    })
  })
})

外部服务集成

Mock 第三方服务

使用 Nock Mock HTTP 请求

typescript
// tests/integration/payment.test.ts
import nock from 'nock'
import PaymentService from '../../src/services/PaymentService'

describe('Payment Service Integration', () => {
  const paymentService = new PaymentService()

  afterEach(() => {
    nock.cleanAll()
  })

  test('should process payment successfully', async () => {
    // Mock Stripe API
    nock('https://api.stripe.com')
      .post('/v1/charges')
      .reply(200, {
        id: 'ch_123',
        amount: 1000,
        currency: 'usd',
        status: 'succeeded'
      })

    const result = await paymentService.charge({
      amount: 1000,
      currency: 'usd',
      source: 'tok_visa'
    })

    expect(result.id).toBe('ch_123')
    expect(result.status).toBe('succeeded')

    // 验证请求被正确调用
    expect(nock.isDone()).toBe(true)
  })

  test('should handle payment failure', async () => {
    nock('https://api.stripe.com')
      .post('/v1/charges')
      .reply(400, {
        error: {
          type: 'card_error',
          message: 'Your card was declined'
        }
      })

    await expect(paymentService.charge({
      amount: 1000,
      currency: 'usd',
      source: 'tok_chargeDeclined'
    })).rejects.toThrow('Your card was declined')
  })

  test('should retry on network error', async () => {
    let callCount = 0

    nock('https://api.stripe.com')
      .post('/v1/charges')
      .times(2)
      .reply(() => {
        callCount++
        if (callCount < 3) {
          return [500, { error: 'Server error' }]
        }
        return [200, { id: 'ch_123', status: 'succeeded' }]
      })

    const result = await paymentService.charge({
      amount: 1000,
      currency: 'usd',
      source: 'tok_visa'
    })

    expect(callCount).toBeGreaterThan(1)
    expect(result.status).toBe('succeeded')
  })
})

Mock AWS S3

typescript
// tests/integration/s3.test.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { mockClient } from 'aws-sdk-client-mock'
import FileService from '../../src/services/FileService'

describe('S3 Integration Tests', () => {
  let s3Mock: any
  let fileService: FileService

  beforeEach(() => {
    s3Mock = mockClient(S3Client)
    fileService = new FileService()
  })

  afterEach(() => {
    s3Mock.restore()
  })

  test('should upload file to S3', async () => {
    s3Mock.on(PutObjectCommand).resolves({
      ETag: '"abc123"',
      Location: 'https://bucket.s3.amazonaws.com/file.jpg'
    })

    const result = await fileService.upload({
      bucket: 'my-bucket',
      key: 'file.jpg',
      body: Buffer.from('file content')
    })

    expect(result.ETag).toBe('"abc123"')
    expect(result.Location).toContain('file.jpg')
  })

  test('should download file from S3', async () => {
    s3Mock.on(GetObjectCommand).resolves({
      Body: Buffer.from('file content')
    })

    const result = await fileService.download({
      bucket: 'my-bucket',
      key: 'file.jpg'
    })

    expect(result.toString()).toBe('file content')
  })
})

使用测试替身

测试替身类型

code
Stub    → 提供预设响应
Mock    → 验证交互行为
Spy     → 记录调用信息
Fake    → 简化实现

Stub 示例

typescript
// tests/integration/email.stub.ts
import IEmailService from '../../src/interfaces/IEmailService'

export class EmailServiceStub implements IEmailService {
  async send(to: string, subject: string, body: string): Promise<boolean> {
    // 总是返回成功
    return true
  }
}
typescript
// tests/integration/notification.test.ts
import { EmailServiceStub } from './email.stub'
import NotificationService from '../../src/services/NotificationService'

describe('Notification Service', () => {
  test('should send notification', async () => {
    const emailStub = new EmailServiceStub()
    const notificationService = new NotificationService(emailStub)

    const result = await notificationService.notify('user@example.com', 'Test message')

    expect(result).toBe(true)
  })
})

Fake 示例

typescript
// tests/integration/cache.fake.ts
export class CacheFake {
  private cache = new Map<string, any>()

  async get(key: string) {
    return this.cache.get(key) || null
  }

  async set(key: string, value: any, ttl?: number) {
    this.cache.set(key, value)
    if (ttl) {
      setTimeout(() => this.cache.delete(key), ttl * 1000)
    }
  }

  async delete(key: string) {
    this.cache.delete(key)
  }

  async clear() {
    this.cache.clear()
  }
}

契约测试

消费者驱动的契约测试

typescript
// tests/integration/pact.consumer.test.ts
import Pact from '@pact-foundation/pact'
import UserService from '../../src/services/UserService'

const provider = new Pact({
  consumer: 'MyApp',
  provider: 'UserAPI',
  port: 1234,
  log: process.cwd() + '/logs/pact.log',
  dir: process.cwd() + '/pacts',
  logLevel: 'error'
})

describe('User Service Contract Tests', () => {
  beforeAll(() => provider.setup())
  afterEach(() => provider.verify())
  afterAll(() => provider.finalize())

  const userService = new UserService('http://localhost:1234')

  test('should get user by id', async () => {
    // 定义期望
    await provider.addInteraction({
      state: 'user exists',
      uponReceiving: 'a request for user',
      withRequest: {
        method: 'GET',
        path: '/api/users/1',
        headers: { Accept: 'application/json' }
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com'
        }
      }
    })

    // 执行请求
    const user = await userService.getUserById(1)

    expect(user).toEqual({
      id: 1,
      name: 'John Doe',
      email: 'john@example.com'
    })
  })

  test('should create user', async () => {
    await provider.addInteraction({
      state: 'user can be created',
      uponReceiving: 'a request to create user',
      withRequest: {
        method: 'POST',
        path: '/api/users',
        headers: { 'Content-Type': 'application/json' },
        body: {
          name: 'John Doe',
          email: 'john@example.com'
        }
      },
      willRespondWith: {
        status: 201,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com'
        }
      }
    })

    const user = await userService.createUser({
      name: 'John Doe',
      email: 'john@example.com'
    })

    expect(user.id).toBeDefined()
  })
})

测试工具与库

supertest

基础用法

typescript
import request from 'supertest'
import app from './app'

// 基础请求
const response = await request(app)
  .get('/api/users')
  .expect(200)

// 设置请求头
const response = await request(app)
  .get('/api/users')
  .set('Authorization', 'Bearer token')
  .set('Accept', 'application/json')
  .expect(200)

// 发送 JSON 数据
const response = await request(app)
  .post('/api/users')
  .send({ name: 'John', email: 'john@example.com' })
  .expect(201)

// 发送表单数据
const response = await request(app)
  .post('/api/login')
  .type('form')
  .send({ username: 'john', password: 'secret' })
  .expect(200)

// 上传文件
const response = await request(app)
  .post('/api/upload')
  .attach('avatar', 'tests/fixtures/avatar.jpg')
  .field('name', 'John')
  .expect(200)

// 断言响应
const response = await request(app)
  .get('/api/users/1')
  .expect('Content-Type', /json/)
  .expect(200, {
    id: 1,
    name: 'John'
  })

// Cookie 支持
const agent = request.agent(app)

// 登录
await agent
  .post('/api/login')
  .send({ username: 'john', password: 'secret' })

// 使用 cookie 访问受保护路由
await agent
  .get('/api/profile')
  .expect(200)

高级用法

typescript
// 自定义断言
test('should validate response structure', async () => {
  const response = await request(app)
    .get('/api/users')
    .expect(200)

  expect(response.body).toBeInstanceOf(Array)
  expect(response.body[0]).toMatchObject({
    id: expect.any(String),
    name: expect.any(String),
    email: expect.stringMatching(/^.+@.+\..+$/)
  })
})

// 超时处理
test('should handle timeout', async () => {
  await request(app)
    .get('/api/slow-endpoint')
    .timeout(5000) // 5秒超时
    .expect(200)
})

// 重定向测试
test('should follow redirect', async () => {
  const response = await request(app)
    .get('/redirect-to-users')
    .expect(302)

  expect(response.headers.location).toBe('/api/users')
})

// 响应时间测试
test('should respond within time limit', async () => {
  const start = Date.now()
  
  await request(app)
    .get('/api/users')
    .expect(200)
  
  const duration = Date.now() - start
  expect(duration).toBeLessThan(1000) // 1秒内
})

mongodb-memory-server

配置选项

typescript
import { MongoMemoryServer } from 'mongodb-memory-server'

// 基础配置
const mongoServer = await MongoMemoryServer.create()

// 自定义配置
const mongoServer = await MongoMemoryServer.create({
  instance: {
    port: 27017,
    dbName: 'test_db',
    storageEngine: 'wiredTiger'
  },
  binary: {
    version: '6.0.0',
    downloadDir: './mongodb-binaries'
  },
  autoStart: false
})

// 获取连接 URI
const uri = mongoServer.getUri()
const dbName = mongoServer.instanceInfo?.dbName

// 停止服务器
await mongoServer.stop()

多数据库支持

typescript
// tests/setup/multi-db.ts
import { MongoMemoryServer } from 'mongodb-memory-server'
import mongoose from 'mongoose'

let mongoServer1: MongoMemoryServer
let mongoServer2: MongoMemoryServer

export async function setupMultiDatabases() {
  mongoServer1 = await MongoMemoryServer.create()
  mongoServer2 = await MongoMemoryServer.create()

  await mongoose.connect(mongoServer1.getUri(), {
    dbName: 'primary'
  })

  return {
    primaryUri: mongoServer1.getUri(),
    secondaryUri: mongoServer2.getUri()
  }
}

export async function cleanupMultiDatabases() {
  await mongoose.disconnect()
  await mongoServer1.stop()
  await mongoServer2.stop()
}

Nock

基础用法

typescript
import nock from 'nock'

// 简单 Mock
nock('https://api.example.com')
  .get('/users')
  .reply(200, { users: [] })

// 匹配请求
nock('https://api.example.com')
  .post('/users', {
    name: 'John',
    email: 'john@example.com'
  })
  .reply(201, { id: 1 })

// 使用正则匹配 URL
nock('https://api.example.com')
  .get(/\/users\/\d+/)
  .reply(200, { id: 1, name: 'John' })

// 动态响应
nock('https://api.example.com')
  .get('/users')
  .reply((uri, requestBody) => {
    return [200, { uri, requestBody }]
  })

// 延迟响应
nock('https://api.example.com')
  .get('/users')
  .delay(1000) // 延迟 1 秒
  .reply(200, { users: [] })

// 验证所有 Mock 都被调用
afterEach(() => {
  expect(nock.isDone()).toBe(true)
  nock.cleanAll()
})

高级用法

typescript
// 持久化 Mock(可重复调用)
nock('https://api.example.com')
  .persist()
  .get('/health')
  .reply(200, { status: 'ok' })

// 请求头匹配
nock('https://api.example.com', {
  reqheaders: {
    authorization: /^Bearer .+$/,
    'content-type': 'application/json'
  }
})
.get('/protected')
.reply(200, { data: 'secret' })

// 错误模拟
nock('https://api.example.com')
  .get('/users')
  .replyWithError('Network error')

// 拦截器
nock('https://api.example.com')
  .get('/users')
  .reply(404)

// 录制模式(用于首次生成 Mock)
nock.recorder.rec()

// 恢复
nock.restore()
nock.recorder.clear()

Faker.js

基础用法

typescript
import { faker } from '@faker-js/faker'

// 个人信息
const name = faker.person.fullName()
const email = faker.internet.email()
const phone = faker.phone.number()
const avatar = faker.image.avatar()

// 地址信息
const address = {
  street: faker.location.streetAddress(),
  city: faker.location.city(),
  country: faker.location.country(),
  zipCode: faker.location.zipCode()
}

// 公司信息
const company = {
  name: faker.company.name(),
  catchPhrase: faker.company.catchPhrase(),
  bs: faker.company.buzzPhrase()
}

// 日期时间
const pastDate = faker.date.past()
const futureDate = faker.date.future()
const recentDate = faker.date.recent()

// 金融信息
const account = {
  accountNumber: faker.finance.accountNumber(),
  creditCard: faker.finance.creditCardNumber(),
  amount: faker.finance.amount()
}

// 随机数据
const randomElement = faker.helpers.arrayElement(['a', 'b', 'c'])
const randomBoolean = faker.datatype.boolean()
const randomNumber = faker.number.int({ min: 1, max: 100 })
const uuid = faker.string.uuid()

自定义 Locale

typescript
import { faker } from '@faker-js/faker/locale/zh_CN'

// 使用中文数据
const name = faker.person.fullName() // 张三
const city = faker.location.city() // 北京

工厂函数集成

typescript
// tests/factories/user.factory.ts
import { faker } from '@faker-js/faker'
import User from '../../src/models/User'

export function createUserData(overrides = {}) {
  return {
    name: faker.person.fullName(),
    email: faker.internet.email(),
    password: faker.internet.password({ length: 12 }),
    age: faker.number.int({ min: 18, max: 80 }),
    phone: faker.phone.number(),
    address: {
      street: faker.location.streetAddress(),
      city: faker.location.city(),
      country: faker.location.country()
    },
    ...overrides
  }
}

export async function createUser(overrides = {}) {
  return User.create(createUserData(overrides))
}

export async function createUsers(count: number, overrides = {}) {
  const users = []
  for (let i = 0; i < count; i++) {
    users.push(createUserData(overrides))
  }
  return User.create(users)
}

测试数据管理

工厂模式

typescript
// tests/factories/order.factory.ts
import { faker } from '@faker-js/faker'
import Order from '../../src/models/Order'
import { createUser } from './user.factory'

export function createOrderData(overrides = {}) {
  return {
    orderNumber: faker.string.alphanumeric(10).toUpperCase(),
    status: faker.helpers.arrayElement(['pending', 'processing', 'completed']),
    total: faker.finance.amount({ min: 10, max: 1000 }),
    items: [
      {
        product: faker.commerce.productName(),
        quantity: faker.number.int({ min: 1, max: 5 }),
        price: faker.finance.amount({ min: 10, max: 100 })
      }
    ],
    ...overrides
  }
}

export async function createOrder(overrides: any = {}) {
  const user = overrides.user || await createUser()
  
  return Order.create({
    ...createOrderData(overrides),
    user: user._id
  })
}

数据固定装置

typescript
// tests/fixtures/users.fixture.ts
export const usersFixture = [
  {
    id: '1',
    name: 'Alice',
    email: 'alice@example.com',
    role: 'admin'
  },
  {
    id: '2',
    name: 'Bob',
    email: 'bob@example.com',
    role: 'user'
  }
]

// tests/integration/fixture.test.ts
import { usersFixture } from '../fixtures/users.fixture'
import User from '../../src/models/User'

describe('Using Fixtures', () => {
  beforeEach(async () => {
    await User.create(usersFixture)
  })

  test('should have fixture users', async () => {
    const users = await User.find()
    expect(users).toHaveLength(2)
  })
})

数据清理策略

typescript
// tests/setup/cleanup.ts
import mongoose from 'mongoose'

export async function cleanupDatabase() {
  const collections = mongoose.connection.collections
  
  for (const key in collections) {
    await collections[key].deleteMany({})
  }
}

export async function cleanupCollections(...collectionNames: string[]) {
  for (const name of collectionNames) {
    await mongoose.connection.collection(name).deleteMany({})
  }
}

// 使用事务回滚
export async function withTransaction(callback: () => Promise<void>) {
  const session = await mongoose.startSession()
  session.startTransaction()
  
  try {
    await callback()
  } finally {
    await session.abortTransaction()
    session.endSession()
  }
}

性能与并发测试

并发场景测试

typescript
// tests/integration/concurrency.test.ts
import request from 'supertest'
import User from '../../src/models/User'

describe('Concurrency Tests', () => {
  test('should handle concurrent requests', async () => {
    const promises = Array(10).fill(null).map((_, i) =>
      request(app)
        .post('/api/users')
        .send({
          name: `User ${i}`,
          email: `user${i}@example.com`,
          password: 'password123'
        })
    )

    const responses = await Promise.all(promises)
    
    responses.forEach(response => {
      expect(response.status).toBe(201)
    })
  })

  test('should handle race condition', async () => {
    const user = await User.create({
      name: 'Alice',
      email: 'alice@example.com',
      balance: 100
    })

    // 同时发起多个更新请求
    const promises = Array(5).fill(null).map(() =>
      request(app)
        .post('/api/transfer')
        .send({
          userId: user._id,
          amount: 20
        })
    )

    const responses = await Promise.all(promises)
    
    // 验证最终状态正确
    const updatedUser = await User.findById(user._id)
    expect(updatedUser.balance).toBe(0) // 100 - (20 * 5) = 0
  })

  test('should handle database connection pool', async () => {
    const promises = Array(20).fill(null).map(() =>
      User.findOne({ email: 'test@example.com' })
    )

    const results = await Promise.all(promises)
    expect(results).toHaveLength(20)
  })
})

性能基准测试

typescript
// tests/integration/performance.test.ts
describe('Performance Benchmarks', () => {
  test('should respond within acceptable time', async () => {
    const start = Date.now()
    
    await request(app)
      .get('/api/users')
      .expect(200)
    
    const duration = Date.now() - start
    console.log(`Duration: ${duration}ms`)
    
    expect(duration).toBeLessThan(1000) // 应在 1 秒内响应
  })

  test('should handle database query efficiently', async () => {
    // 创建 1000 条测试数据
    await User.create(
      Array(1000).fill(null).map((_, i) => ({
        name: `User ${i}`,
        email: `user${i}@example.com`,
        password: 'pass'
      }))
    )

    const start = Date.now()
    
    const users = await User.find()
      .limit(50)
      .skip(0)
      .sort({ name: 1 })
    
    const duration = Date.now() - start
    
    expect(users).toHaveLength(50)
    expect(duration).toBeLessThan(100) // 查询应在 100ms 内完成
  })

  test('should handle bulk operations', async () => {
    const users = Array(1000).fill(null).map((_, i) => ({
      name: `User ${i}`,
      email: `user${i}@example.com`,
      password: 'pass'
    }))

    const start = Date.now()
    
    await User.insertMany(users)
    
    const duration = Date.now() - start
    console.log(`Bulk insert duration: ${duration}ms`)
    
    expect(duration).toBeLessThan(5000) // 应在 5 秒内完成
  })
})

CI/CD 集成

GitHub Actions 配置

yaml
# .github/workflows/integration-tests.yml
name: Integration Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mongodb:
        image: mongo:6.0
        ports:
          - 27017:27017
        options: >-
          --health-cmd "echo 'db.runCommand("ping").ok' | mongo localhost:27017/test --quiet"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      
      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    strategy:
      matrix:
        node-version: [18.x, 20.x]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Run integration tests
        run: pnpm test:integration
        env:
          NODE_ENV: test
          MONGODB_URI: mongodb://localhost:27017/test
          REDIS_URL: redis://localhost:6379
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          flags: integration

Docker 集成测试

yaml
# docker-compose.test.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      - mongodb
      - redis
    environment:
      NODE_ENV: test
      MONGODB_URI: mongodb://mongodb:27017/test
      REDIS_URL: redis://redis:6379
    volumes:
      - ./coverage:/app/coverage

  mongodb:
    image: mongo:6.0
    ports:
      - "27017:27017"

  redis:
    image: redis:7
    ports:
      - "6379:6379"
dockerfile
# Dockerfile.test
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npm", "run", "test:integration"]
bash
# 运行 Docker 集成测试
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit

最佳实践

测试原则

1. 测试独立性

typescript
// ❌ 错误:测试相互依赖
describe('Bad Example', () => {
  let userId: string

  test('create user', async () => {
    const user = await createUser()
    userId = user.id // 下一个测试依赖这个值
  })

  test('get user', async () => {
    const user = await getUser(userId) // 依赖上一个测试
  })
})

// ✅ 正确:每个测试独立
describe('Good Example', () => {
  beforeEach(async () => {
    await clearDatabase()
  })

  test('create user', async () => {
    const user = await createUser()
    expect(user.id).toBeDefined()
  })

  test('get user', async () => {
    const created = await createUser()
    const user = await getUser(created.id)
    expect(user.id).toBe(created.id)
  })
})

2. 数据清理

typescript
// ✅ 使用 afterEach 清理
afterEach(async () => {
  await clearCollections()
})

// ✅ 使用事务回滚
describe('Transaction Tests', () => {
  let session: any

  beforeEach(async () => {
    session = await mongoose.startSession()
    session.startTransaction()
  })

  afterEach(async () => {
    await session.abortTransaction()
    session.endSession()
  })

  test('should create user', async () => {
    await User.create([userData], { session })
  })
})

3. 环境隔离

typescript
// ✅ 使用独立的测试环境
process.env.NODE_ENV = 'test'
process.env.DB_NAME = 'test_db'

// ✅ 使用内存数据库
const mongoServer = await MongoMemoryServer.create()

// ✅ Mock 外部服务
nock('https://api.external.com')
  .get('/data')
  .reply(200, { data: 'mocked' })

代码组织

code
tests/
├── integration/              # 集成测试
│   ├── api/                  # API 测试
│   │   ├── user.api.test.ts
│   │   └── auth.api.test.ts
│   ├── database/             # 数据库测试
│   │   ├── user.db.test.ts
│   │   └── order.db.test.ts
│   └── services/             # 服务测试
│       └── payment.test.ts
├── setup/                    # 测试配置
│   ├── db.ts
│   └── app.ts
├── fixtures/                 # 测试数据
│   └── users.fixture.ts
├── factories/                # 数据工厂
│   ├── user.factory.ts
│   └── order.factory.ts
├── mocks/                    # Mock 实现
│   └── email.mock.ts
├── setup.ts                  # Jest 设置
├── globalSetup.ts            # 全局初始化
└── globalTeardown.ts         # 全局清理

测试隔离

使用 describe 分组

typescript
describe('User API', () => {
  describe('GET /api/users', () => {
    test('should return all users', async () => {
      // ...
    })

    test('should filter users by role', async () => {
      // ...
    })
  })

  describe('POST /api/users', () => {
    test('should create user', async () => {
      // ...
    })

    test('should validate input', async () => {
      // ...
    })
  })
})

使用 beforeEach/afterEach

typescript
describe('User Service', () => {
  let userService: UserService

  beforeEach(() => {
    userService = new UserService()
  })

  describe('createUser', () => {
    test('should create user', async () => {
      const user = await userService.create(userData)
      expect(user).toBeDefined()
    })
  })
})

常见问题

1. 如何处理数据库连接?

typescript
// 在 globalSetup 中连接,globalTeardown 中关闭
export default async function globalSetup() {
  await mongoose.connect(process.env.MONGO_URI!)
}

export default async function globalTeardown() {
  await mongoose.disconnect()
}

2. 如何测试文件上传?

typescript
test('should upload file', async () => {
  const response = await request(app)
    .post('/api/upload')
    .attach('file', 'tests/fixtures/test-image.jpg')
    .expect(200)

  expect(response.body).toHaveProperty('url')
})

3. 如何测试 WebSocket?

typescript
import { io } from 'socket.io-client'

describe('WebSocket Tests', () => {
  let socket: any

  beforeEach((done) => {
    socket = io('http://localhost:3000')
    socket.on('connect', done)
  })

  afterEach(() => {
    socket.disconnect()
  })

  test('should receive message', (done) => {
    socket.emit('message', 'Hello')
    socket.on('message', (msg: string) => {
      expect(msg).toBe('Hello')
      done()
    })
  })
})

4. 如何测试定时任务?

typescript
import { jest } from '@jest/globals'

describe('Scheduled Tasks', () => {
  jest.useFakeTimers()

  afterEach(() => {
    jest.clearAllTimers()
  })

  test('should run task at scheduled time', () => {
    const callback = jest.fn()
    
    setInterval(callback, 60000)
    
    jest.advanceTimersByTime(60000)
    
    expect(callback).toHaveBeenCalled()
  })
})

5. 如何处理异步错误?

typescript
test('should handle async error', async () => {
  await expect(asyncOperation()).rejects.toThrow('Expected error')
})

// 或者使用 try-catch
test('should catch error', async () => {
  try {
    await asyncOperation()
    fail('Should have thrown')
  } catch (error) {
    expect(error.message).toBe('Expected error')
  }
})

6. 如何测试多步骤流程?

typescript
describe('Order Flow', () => {
  let orderId: string

  test('Step 1: Create order', async () => {
    const response = await request(app)
      .post('/api/orders')
      .send(orderData)
      .expect(201)

    orderId = response.body.id
  })

  test('Step 2: Pay order', async () => {
    await request(app)
      .post(`/api/orders/${orderId}/pay`)
      .send(paymentData)
      .expect(200)
  })

  test('Step 3: Confirm order', async () => {
    const response = await request(app)
      .get(`/api/orders/${orderId}`)
      .expect(200)

    expect(response.body.status).toBe('paid')
  })
})

故障排查

常见错误

1. 数据库连接失败

code
Error: connect ECONNREFUSED 127.0.0.1:27017

解决方案

typescript
// 检查数据库是否运行
// 使用内存数据库或启动本地 MongoDB
await MongoMemoryServer.create()

// 或增加连接超时
mongoose.connect(uri, {
  serverSelectionTimeoutMS: 5000
})

2. Jest 超时错误

code
Timeout - Async callback was not invoked within the 5000 ms timeout

解决方案

typescript
// 增加超时时间
jest.setTimeout(30000)

// 或在测试中设置
test('long running test', async () => {
  // ...
}, 30000) // 30 秒超时

3. 端口被占用

code
Error: listen EADDRINUSE: address already in use :::3000

解决方案

typescript
// 使用随机端口
const app = express()
const server = app.listen(0) // 随机分配端口
const port = server.address().port

// 或在测试中使用不同端口
const PORT = process.env.TEST_PORT || 3001

4. 未清理的数据库连接

code
Jest did not exit one second after the test run has completed.

解决方案

typescript
// 确保关闭所有连接
afterAll(async () => {
  await mongoose.disconnect()
  await mongoServer.stop()
})

// 或使用 --forceExit
jest --forceExit --detectOpenHandles

5. Mock 不生效

code
Expected mock function to have been called, but it was not called

解决方案

typescript
// 确保 Mock 在测试前设置
beforeEach(() => {
  jest.clearAllMocks()
  nock.cleanAll()
})

// 检查 Mock 路径是否正确
jest.mock('../../src/services/EmailService')

调试技巧

1. 使用调试日志

typescript
// 在测试中添加日志
test('debug test', async () => {
  const user = await User.findOne({ email: 'test@example.com' })
  console.log('Found user:', user)
  
  const response = await request(app).get('/api/users')
  console.log('Response:', response.body)
})

2. 使用 Jest 调试器

bash
# 运行单个测试
jest --runInBand --detectOpenHandles user.test.ts

# 显示详细输出
jest --verbose

# 显示覆盖率
jest --coverage

3. 使用 Node 调试器

bash
# 在 VSCode 中调试
node --inspect-brk node_modules/.bin/jest --runInBand

# 添加到 launch.json
{
  "type": "node",
  "request": "launch",
  "name": "Jest Debug",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand", "--no-cache"],
  "console": "integratedTerminal"
}

4. 检查数据库状态

typescript
// 打印数据库状态
async function debugDatabase() {
  const collections = await mongoose.connection.db.listCollections().toArray()
  console.log('Collections:', collections.map(c => c.name))
  
  const userCount = await User.countDocuments()
  console.log('User count:', userCount)
}

test('debug database', async () => {
  await debugDatabase()
})

清理缓存

bash
# 清理 Jest 缓存
jest --clearCache

# 清理 node_modules
rm -rf node_modules
npm install

# 清理 dist/build
rm -rf dist build

参考资源


Node.js 22+ 测试新特性

node:test 内置测试框架

Node.js 22+ 内置 node:test,无需安装 Jest/Mocha 即可进行集成测试:

javascript
import { describe, it, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'

describe('API 集成测试', () => {
  let server

  beforeEach(async () => {
    server = await startTestServer()
  })

  afterEach(async () => {
    await server.close()
  })

  it('GET /api/users 返回用户列表', async () => {
    const response = await fetch('http://localhost:3000/api/users')
    assert.strictEqual(response.status, 200)
    const data = await response.json()
    assert.ok(Array.isArray(data))
  })

  it('POST /api/users 创建新用户', async () => {
    const response = await fetch('http://localhost:3000/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Test User' })
    })
    assert.strictEqual(response.status, 201)
    const user = await response.json()
    assert.strictEqual(user.name, 'Test User')
  })
})
bash
# 运行测试
node --test

# 运行指定测试文件
node --test test/integration/**/*.test.js

# 监视模式
node --test --watch

# 覆盖率报告
node --test --experimental-test-coverage