2. NestJS 集成测试环境搭建:使用 @nestjs/testing 创建测试模块

集成测试的核心在于构建一个接近真实运行环境的测试上下文,而非仅对单个类进行孤立验证。NestJS 提供了 @nestjs/testing 包,通过 Test.createTestingModule 方法,允许开发者以声明式的方式组合真实模块、模拟模块以及自定义 Provider,从而快速搭建可复用的测试沙箱。

2.1 测试模块的核心构建流程

使用 @nestjs/testing 搭建集成测试环境通常遵循以下四个步骤,每一步都直接影响测试的隔离性与真实性平衡。

  1. 创建测试模块实例:调用 Test.createTestingModule({...}) 传入模块元数据。
  2. 编译模块:调用 .compile() 方法,返回一个已解析的 TestingModule 实例。
  3. 获取目标实例:通过 module.get<T>(Token) 获取控制器、服务或守卫等实例。
  4. 执行断言:在 it 块中调用目标方法并验证结果。
关键原则: 集成测试应优先导入真实模块(如数据库连接、核心业务逻辑),仅对副作用明显的依赖(如邮件服务、第三方 API 客户端)使用 Mock 模块。

2.2 配置 Test.createTestingModule 的元数据

Test.createTestingModule 接受的元数据对象结构与标准 @Module 装饰器完全一致,包含 imports、controllers、providers、exports 四个核心属性。下表对比了集成测试中不同元数据字段的典型用法与注意事项:

元数据字段 集成测试典型用法 注意事项
imports 导入真实模块(如 TypeOrmModule、ConfigModule)或共享模块 避免导入整个 AppModule,应只导入被测功能依赖的最小模块集合
controllers 通常导入被测控制器,用于端到端请求验证 若仅测试服务层,可省略 controllers 以加快编译速度
providers 注册真实服务、Repository 或自定义 Provider 使用 useValue / useFactory 替换外部依赖为 Mock 对象
exports 测试模块通常不需要 exports,除非被其他测试模块引用 保持测试模块扁平化,避免模块嵌套过深

2.3 导入真实模块与 Mock 模块的策略

在集成测试中,如何平衡真实逻辑与模拟依赖是架构设计的核心决策。以下策略基于实际压测场景总结:

  • 真实模块优先:数据库连接(TypeORM / Prisma)、缓存(Redis)、消息队列(RabbitMQ)等基础设施模块应导入真实实例,确保 SQL 查询、事务行为与生产环境一致。
  • Mock 外部副作用:邮件发送、短信通知、第三方支付网关等对外部系统有强依赖的服务,应使用 @Injectable() 的 Mock 类或 useValue 注入模拟对象。
  • 使用自定义 Provider 覆盖:通过 providers 字段中定义与真实模块相同 Token 的 Provider,可以局部替换某个依赖而不影响模块整体结构。
实践建议: 在压测场景中,建议将数据库操作保留为真实模块,因为压测的核心目标之一是验证数据持久化层的性能与稳定性。Mock 数据库会丢失大量有价值的性能瓶颈信息。

2.4 集成测试模块架构示意

下图展示了一个典型的 NestJS 集成测试模块结构,其中真实模块与 Mock 模块通过 Test.createTestingModule 组合在一起:

NestJS 集成测试模块架构图 集成测试模块组合架构 Test.createTestingModule 容器 真实模块 (Real Modules) TypeOrmModule.forRoot() ConfigModule.forRoot() 真实 UserService Mock 模块 (Mock Modules) MailService (useValue) PaymentGateway (useFactory) Mock 外部 API 客户端 测试执行层 (Test Runner) 通过 module.get() 获取实例 → 执行断言 说明:真实模块保留数据库与核心业务逻辑,Mock 模块仅替换外部副作用依赖

2.5 代码示例:搭建一个包含真实数据库与 Mock 邮件服务的测试模块

以下代码展示了如何在集成测试中同时导入 TypeORM 真实模块,并使用 useValue 注入一个模拟的邮件服务:

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService } from './user.service';
import { User } from './user.entity';
import { MailService } from './mail.service';

// 创建 Mock 邮件服务对象
const mockMailService = {
  sendWelcomeEmail: jest.fn().mockResolvedValue(true),
};

describe('UserService Integration Test', () => {
  let module: TestingModule;
  let userService: UserService;
  let userRepository: Repository<User>;

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [
        // 导入真实 TypeORM 模块,连接测试数据库
        TypeOrmModule.forRoot({
          type: 'postgres',
          host: 'localhost',
          port: 5432,
          username: 'test_user',
          password: 'test_pass',
          database: 'test_db',
          entities: [User],
          synchronize: true, // 仅用于测试环境
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [
        UserService,
        {
          provide: MailService,
          useValue: mockMailService, // 使用 Mock 替换真实邮件服务
        },
      ],
    }).compile();

    userService = module.get<UserService>(UserService);
    userRepository = module.get<Repository<User>>(getRepositoryToken(User));
  });

  afterAll(async () => {
    await module.close(); // 关闭数据库连接
  });

  it('should create a user and send welcome email', async () => {
    const user = await userService.createUser({
      name: 'Test User',
      email: 'test@example.com',
    });

    expect(user).toBeDefined();
    expect(user.name).toBe('Test User');
    expect(mockMailService.sendWelcomeEmail).toHaveBeenCalledWith(user.email);
  });
});

2.6 真实模块与 Mock 模块的决策矩阵

在实际项目中,不同依赖的测试策略选择直接影响测试的可靠性与执行速度。以下矩阵总结了常见场景的推荐做法:

依赖类型 推荐策略 理由 压测场景建议
关系型数据库 (PostgreSQL / MySQL) 真实模块 验证 SQL 语法、事务、索引行为 必须使用真实数据库
NoSQL 数据库 (MongoDB / Redis) 真实模块 验证查询模式与缓存策略 建议使用真实实例
邮件 / 短信服务 Mock 模块 避免发送真实请求,提升测试速度 Mock 即可
第三方 REST / gRPC API Mock 模块 消除外部网络波动影响 Mock 或使用 WireMock
消息队列 (RabbitMQ / Kafka) 真实模块 验证消息发布与消费的完整性 建议使用真实队列
文件存储 (S3 / 本地文件系统) Mock 模块 避免磁盘 I/O 与权限问题 Mock 或使用内存文件系统

2.7 常见陷阱与避坑指南

陷阱一: 在 beforeAll 中编译模块后,忘记在 afterAll 中调用 module.close(),导致数据库连接未释放,后续测试出现连接池耗尽错误。
陷阱二: 过度使用 Mock 导致测试失去集成意义。例如 Mock 了 Repository 的 save 方法,使得测试无法捕获实体校验失败或数据库约束违反等真实错误。
最佳实践: 为每个集成测试模块创建独立的 beforeAll 块,并使用 jest.resetAllMocks() 确保 Mock 状态在测试间隔离。对于大型项目,可封装 createTestingModule 辅助函数,统一管理常用模块的导入与 Mock 配置。

2.8 测试模块生命周期管理

为了在压测场景中保持测试环境的稳定性,需要清晰管理测试模块的创建与销毁。下图展示了推荐的测试模块生命周期流程:

测试模块生命周期流程图 测试模块生命周期管理流程 1. 创建 TestingModule 2. 编译模块 3. 获取服务实例 4. 执行测试用例 5. 关闭模块 afterAll 中必须调用 module.close() 释放资源

遵循上述生命周期管理,可以确保每个测试套件在独立的模块上下文中运行,避免模块状态污染,同时保证数据库连接等资源被正确回收,为后续压测场景的稳定执行奠定基础。