Suites
Suites 是一个面向 TypeScript 依赖注入框架的开源单元测试框架。它可以作为手动创建 mock、编写冗长的多重 mock 配置测试、或使用无类型测试替身(如 mocks 和 stubs)的替代方案。
Suites 会在运行时读取 nestjs service 的元数据,并自动为所有依赖生成完整类型的 mock。 这消除了样板 mock 配置代码,并确保测试类型安全。虽然 Suites 可以和 Test.createTestingModule() 一起使用,但它更擅长聚焦型单元测试。 当你需要验证模块装配、装饰器、守卫和拦截器时,使用 Test.createTestingModule()。 当你需要快速进行自动 mock 生成的单元测试时,使用 Suites。
关于基于模块的测试,参见测试基础章节。
注意
Suites 是第三方包,并不由 NestJS 核心团队维护。如有问题,请反馈到其对应仓库。
开始使用
本指南演示如何使用 Suites 测试 NestJS services。内容同时覆盖隔离式测试(所有依赖都被 mock)和社交式测试(部分依赖使用真实实现)。
安装 Suites
先确认已安装 NestJS 运行时依赖:
$ npm install @nestjs/common @nestjs/core reflect-metadata安装 Suites 核心、NestJS 适配器以及 doubles 适配器:
$ npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.jestdoubles 适配器(@suites/doubles.jest)提供了对 Jest mock 能力的封装。它暴露了 mock() 和 stub() 函数,用于创建类型安全的测试替身。
同时确保 Jest 和 TypeScript 可用:
$ npm install --save-dev ts-jest @types/jest jest typescript如果你使用的是 Vitest,请展开
$ npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.vitest如果你使用的是 Sinon,请展开
$ npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.sinon设置类型定义
在项目根目录创建 global.d.ts:
/// <reference types="@suites/doubles.jest/unit" />
/// <reference types="@suites/di.nestjs/types" />创建示例 service
本指南使用一个带有两个依赖的简单 UserService:
// user.repository.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository {
async findById(id: string): Promise<User | null> {
// Database query
}
async save(user: User): Promise<User> {
// Database save
}
}// user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { Logger } from '@nestjs/common';
@Injectable()
export class UserService {
constructor(
private repository: UserRepository,
private logger: Logger,
) {}
async findById(id: string): Promise<User> {
const user = await this.repository.findById(id);
if (!user) {
throw new NotFoundException(`User ${id} not found`);
}
this.logger.log(`Found user ${id}`);
return user;
}
async create(email: string, name: string): Promise<User> {
const user = { id: generateId(), email, name };
await this.repository.save(user);
this.logger.log(`Created user ${user.id}`);
return user;
}
}编写单元测试
使用 TestBed.solitary() 创建隔离式测试,其中所有依赖都会自动被 mock:
// user.service.spec.ts
import { TestBed, type Mocked } from '@suites/unit';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { Logger } from '@nestjs/common';
describe('User Service Unit Spec', () => {
let userService: UserService;
let repository: Mocked<UserRepository>;
let logger: Mocked<Logger>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(UserService).compile();
userService = unit;
repository = unitRef.get(UserRepository);
logger = unitRef.get(Logger);
});
it('should find user by id', async () => {
const user = { id: '1', email: 'test@example.com', name: 'Test' };
repository.findById.mockResolvedValue(user);
const result = await userService.findById('1');
expect(result).toEqual(user);
expect(logger.log).toHaveBeenCalled();
});
});TestBed.solitary() 会分析构造函数,并为所有依赖创建带类型的 mock。 Mocked<T> 类型则为 mock 配置提供 IntelliSense 支持。
预编译 mock 配置
你可以使用 .mock().impl() 在编译前就配置好 mock 行为:
// user.service.spec.ts
import { TestBed } from '@suites/unit';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
describe('User Service Unit Spec - pre-configured', () => {
let unit: UserService;
let repository: Mocked<UserRepository>;
beforeAll(async () => {
const { unit: underTest, unitRef } = await TestBed.solitary(UserService)
.mock(UserRepository)
.impl(stubFn => ({
findById: stubFn().mockResolvedValue({ id: '1', email: 'test@example.com', name: 'Test' })
}))
.compile();
repository = unitRef.get(UserRepository);
unit = underTest;
})
it('should find user with pre-configured mock', async () => {
const result = await unit.findById('1');
expect(repository.findById).toHaveBeenCalled();
expect(result.email).toBe('test@example.com');
});
});stubFn 参数会对应你安装的 doubles 适配器(Jest 时是 jest.fn(),Vitest 时是 vi.fn(),Sinon 时是 sinon.stub())。
使用真实依赖进行测试
使用 TestBed.sociable() 配合 .expose(),即可让某些依赖使用真实实现:
// user.service.spec.ts
import { TestBed, Mocked } from '@suites/unit';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { Logger } from '@nestjs/common';
describe('UserService - with real logger', () => {
let userService: UserService;
let repository: Mocked<UserRepository>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.sociable(UserService)
.expose(Logger)
.compile();
userService = unit;
repository = unitRef.get(UserRepository);
});
it('should log when finding user', async () => {
const user = { id: '1', email: 'test@example.com' };
repository.findById.mockResolvedValue(user);
await userService.findById('1');
// Logger actually executes, no mock needed
});
});.expose(Logger) 会用真实实现实例化 Logger,而其他依赖依然保持 mock。
基于 token 的依赖
Suites 可以处理自定义注入 token(字符串或 symbol):
// config.service.ts
import { Injectable, Inject } from '@nestjs/common';
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
@Injectable()
export class ConfigService {
constructor(
@Inject(CONFIG_OPTIONS) private options: { apiKey: string },
) {}
getApiKey(): string {
return this.options.apiKey;
}
}通过 unitRef.get() 获取基于 token 的依赖:
// config.service.spec.ts
import { TestBed } from '@suites/unit';
import { ConfigService, CONFIG_OPTIONS, ConfigOptions } from './config.service';
describe('Config Service Unit Spec', () => {
let configService: ConfigService;
let options: ConfigOptions;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(ConfigService).compile();
configService = unit;
options = unitRef.get<ConfigOptions>(CONFIG_OPTIONS);
});
it('should return api key', () => { ... });
});直接使用 mock() 和 stub()
如果你更喜欢不通过 TestBed 而直接控制 mock,对应的 doubles 适配器也提供了 mock() 和 stub() 函数:
// user.service.spec.ts
import { mock } from '@suites/unit';
import { UserRepository } from './user.repository';
describe('User Service Unit Spec', () => {
it('should work with direct mocks', async () => {
const repository = mock<UserRepository>();
const logger = mock<Logger>();
const service = new UserService(repository, logger);
// ...
});
});mock() 会创建一个带类型的 mock 对象,而 stub() 会包装底层的 mock 库(本例中是 Jest),从而提供诸如 mockResolvedValue() 这样的方法。 这些函数来自你安装的 doubles 适配器(如 @suites/doubles.jest),它负责把测试框架原生的 mock 能力适配进来。
提示
mock() 是 @golevelup/ts-jest 中 createMock 的替代方案。两者都可以创建带类型的 mock 对象。关于 createMock 的更多内容,请参见测试基础章节。
总结
在以下情况使用 Test.createTestingModule():
- 验证模块配置和 provider 装配
- 测试装饰器、守卫、拦截器和管道
- 验证跨模块依赖注入
- 测试带中间件的完整应用上下文
在以下情况使用 Suites:
- 面向业务逻辑的快速单元测试
- 为多个依赖自动生成 mock
- 使用带 IntelliSense 的类型安全测试替身
按目的组织测试:用 Suites 编写针对单个 service 行为的单元测试;用 Test.createTestingModule() 编写验证模块配置的集成测试。
更多信息: