Automock TypeScript Reflection API

导读

Automock 是一个功能强大的独立库,专为单元测试而设计。它在内部利用 TypeScript Reflection API 来生成模拟对象,通过自动模拟类的外部依赖项来简化测试过程。Automock 使您能够简化测试开发并专注于编写强大而高效的单元测试。

info

Automock 是第三方包,不受 NestJS 核心团队管理。

请在 适当的存储库 中报告发现的与库相关的任何问题

简介

依赖注入 (DI) 容器是 Nest 模块系统的基础元素,对于应用程序运行时和测试阶段都不可或缺。在单元测试中,模拟依赖项对于隔离和评估特定组件的行为至关重要。但是,这些模拟对象的手动配置和管理可能很复杂,并且容易出错。

Automock 提供了一种简化的解决方案。 Automock 不会与实际的 Nest DI 容器交互,而是引入了一个虚拟容器,依赖项会自动模拟。这种方法绕过了手动任务,即用模拟实现替换 DI 容器中的每个提供程序。使用 Automock,可以自动生成所有依赖项的模拟对象,从而简化单元测试设置过程。

安装

Automock 同时支持 Jest 和 Sinon。只需为您选择的测试框架安装适当的软件包即可。 此外,您还需要安装 @automock/adapters.nestjs(因为 Automock 支持其他适配器)。

bash
$ npm i -D @automock/jest @automock/adapters.nestjs

Or, for Sinon:

bash
$ npm i -D @automock/sinon @automock/adapters.nestjs

示例

此处提供的示例展示了 Automock 与 Jest 的集成。但是,相同的原则和功能也适用于 Sinon。

考虑以下 CatService 类,它依赖于 Database 类来获取猫。我们将模拟 Database 类以单独测试 CatsService 类。

ts
@Injectable()
export class Database {
  getCats(): Promise<Cat[]> { ... }
}

@Injectable()
class CatsService {
  constructor(private database: Database) {}

  async getAllCats(): Promise<Cat[]> {
    return this.database.getCats();
  }
}

让我们为CatsService类设置一个单元测试。

我们将使用@automock/jest包中的TestBed来创建我们的测试环境。

ts
import { TestBed } from '@automock/jest'

describe('Cats Service Unit Test', () => {
  let catsService: CatsService
  let database: jest.Mocked<Database>

  beforeAll(() => {
    const { unit, unitRef } = TestBed.create(CatsService).compile()

    catsService = unit
    database = unitRef.get(Database)
  })

  it('should retrieve cats from the database', async () => {
    const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }]
    database.getCats.mockResolvedValue(mockCats)

    const cats = await catsService.getAllCats()

    expect(database.getCats).toHaveBeenCalled()
    expect(cats).toEqual(mockCats)
  })
})

在测试设置中,我们:

  1. 使用 TestBed.create(CatsService).compile()CatsService 创建测试环境。
  2. 分别使用 unitunitRef.get(Database) 获取 CatsService 的实际实例和 Database 的模拟实例。
  3. 我们模拟 Database 类的 getCats 方法以返回预定义的猫列表。
  4. 然后,我们调用 CatsServicegetAllCats 方法并验证它是否正确与 Database 类交互并返回预期的猫。

添加记录器

让我们通过添加 Logger 接口并将其集成到 CatsService 类中来扩展我们的示例。

ts
@Injectable()
class Logger {
  log(message: string): void { ... }
}

@Injectable()
class CatsService {
  constructor(private database: Database, private logger: Logger) {}

  async getAllCats(): Promise<Cat[]> {
    this.logger.log('Fetching all cats..');
    return this.database.getCats();
  }
}

现在,当你设置测试时,你还需要模拟Logger依赖项:

ts
beforeAll(() => {
  let logger: jest.Mocked<Logger>
  const { unit, unitRef } = TestBed.create(CatsService).compile()

  catsService = unit
  database = unitRef.get(Database)
  logger = unitRef.get(Logger)
})

it('should log a message and retrieve cats from the database', async () => {
  const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }]
  database.getCats.mockResolvedValue(mockCats)

  const cats = await catsService.getAllCats()

  expect(logger.log).toHaveBeenCalledWith('Fetching all cats..')
  expect(database.getCats).toHaveBeenCalled()
  expect(cats).toEqual(mockCats)
})

使用 .mock().using() 进行模拟实现

Automock 提供了一种更具声明性的方式来使用 .mock().using() 方法链指定模拟实现。 这允许您在设置 TestBed 时直接定义模拟行为。

您可以按照以下方法修改测试设置以使用此方法:

ts
beforeAll(() => {
  const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }]

  const { unit, unitRef } = TestBed.create(CatsService)
    .mock(Database)
    .using({ getCats: async () => mockCats })
    .compile()

  catsService = unit
  database = unitRef.get(Database)
})

通过这种方法,我们无需在测试主体中手动模拟 getCats 方法。 相反,我们使用 .mock().using() 直接在测试设置中定义模拟行为。

依赖项引用和实例访问

使用 TestBed 时,compile() 方法返回一个具有两个重要属性的对象:unitunitRef。 这些属性分别提供对被测类实例的访问和对其依赖项的引用。

unit - unit 属性表示被测类的实际实例。在我们的示例中,它对应于 CatsService 类的一个实例。这允许您在测试场景中直接与该类交互并调用其方法。

unitRef - unitRef 属性用作对被测类依赖项的引用。在我们的示例中,它引用了 CatsService 使用的 Logger 依赖项。通过访问 unitRef,您可以检索依赖项的自动生成的模拟对象。这使您能够存根方法、定义行为并在模拟对象上断言方法调用。

使用不同的提供程序

提供程序是 Nest 中最重要的元素之一。您可以将许多默认 Nest 类视为提供程序,包括服务、存储库、工厂、助手等。提供程序的主要功能是采用Injectable依赖项的形式。

考虑以下CatsService,它接受一个参数,该参数是以下Logger接口的一个实例:

ts
export interface Logger {
  log: (message: string) => void
}

@Injectable()
export class CatsService {
  constructor(private logger: Logger) {}
}

TypeScript 的 Reflection API 尚不支持接口反射。Nest 使用基于字符串/符号的注入令牌解决了这个问题(请参阅 自定义提供程序):

ts
export const MyLoggerProvider = {
  provide: 'LOGGER_TOKEN',
  useValue: { ... },
}

@Injectable()
export class CatsService {
  constructor(@Inject('LOGGER_TOKEN') readonly logger: Logger) {}
}

】 Automock 遵循这种做法,并允许您提供基于字符串(或基于符号)的标记,而不是在 unitRef.get() 方法中提供实际的类:

ts
const { unit, unitRef } = TestBed.create(CatsService).compile()

const loggerMock: jest.Mocked<Logger> = unitRef.get('LOGGER_TOKEN')

更多信息

请访问 Automock GitHub 存储库Automock 网站 了解更多信息。