Skip to content

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 运行时依赖:

bash
$ npm install @nestjs/common @nestjs/core reflect-metadata

安装 Suites 核心、NestJS 适配器以及 doubles 适配器:

bash
$ npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.jest

doubles 适配器(@suites/doubles.jest)提供了对 Jest mock 能力的封装。它暴露了 mock() 和 stub() 函数,用于创建类型安全的测试替身。

同时确保 Jest 和 TypeScript 可用:

bash
$ npm install --save-dev ts-jest @types/jest jest typescript
如果你使用的是 Vitest,请展开
bash
$ npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.vitest
如果你使用的是 Sinon,请展开
bash
$ npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.sinon

设置类型定义 ​

在项目根目录创建 global.d.ts:

typescript
/// <reference types="@suites/doubles.jest/unit" />
/// <reference types="@suites/di.nestjs/types" />

创建示例 service ​

本指南使用一个带有两个依赖的简单 UserService:

typescript
// 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
  }
}
typescript
// 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:

typescript
// 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 行为:

typescript
// 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(),即可让某些依赖使用真实实现:

typescript
// 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):

typescript
// 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 的依赖:

typescript
// 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() 函数:

typescript
// 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() 编写验证模块配置的集成测试。

更多信息:

基于 NestJS 官方文档翻译