2. NestJS 集成测试环境搭建:使用 @nestjs/testing 创建测试模块
集成测试的核心在于构建一个接近真实运行环境的测试上下文,而非仅对单个类进行孤立验证。NestJS 提供了 @nestjs/testing 包,通过 Test.createTestingModule 方法,允许开发者以声明式的方式组合真实模块、模拟模块以及自定义 Provider,从而快速搭建可复用的测试沙箱。
2.1 测试模块的核心构建流程
使用 @nestjs/testing 搭建集成测试环境通常遵循以下四个步骤,每一步都直接影响测试的隔离性与真实性平衡。
- 创建测试模块实例:调用
Test.createTestingModule({...})传入模块元数据。 - 编译模块:调用
.compile()方法,返回一个已解析的TestingModule实例。 - 获取目标实例:通过
module.get<T>(Token)获取控制器、服务或守卫等实例。 - 执行断言:在
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 组合在一起:
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 测试模块生命周期管理
为了在压测场景中保持测试环境的稳定性,需要清晰管理测试模块的创建与销毁。下图展示了推荐的测试模块生命周期流程:
遵循上述生命周期管理,可以确保每个测试套件在独立的模块上下文中运行,避免模块状态污染,同时保证数据库连接等资源被正确回收,为后续压测场景的稳定执行奠定基础。