Automock 是一个功能强大的独立库,专为单元测试而设计。它在内部利用 TypeScript Reflection API 来生成模拟对象,通过自动模拟类的外部依赖项来简化测试过程。Automock 使您能够简化测试开发并专注于编写强大而高效的单元测试。
Automock
是第三方包,不受 NestJS 核心团队管理。
请在 适当的存储库 中报告发现的与库相关的任何问题
简介
依赖注入 (DI) 容器是 Nest 模块系统的基础元素,对于应用程序运行时和测试阶段都不可或缺。在单元测试中,模拟依赖项对于隔离和评估特定组件的行为至关重要。但是,这些模拟对象的手动配置和管理可能很复杂,并且容易出错。
Automock 提供了一种简化的解决方案。 Automock 不会与实际的 Nest DI 容器交互,而是引入了一个虚拟容器,依赖项会自动模拟。这种方法绕过了手动任务,即用模拟实现替换 DI 容器中的每个提供程序。使用 Automock,可以自动生成所有依赖项的模拟对象,从而简化单元测试设置过程。
安装
Automock 同时支持 Jest 和 Sinon。只需为您选择的测试框架安装适当的软件包即可。
此外,您还需要安装 @automock/adapters.nestjs
(因为 Automock 支持其他适配器)。
$ npm i -D @automock/jest @automock/adapters.nestjs
Or, for Sinon:
$ npm i -D @automock/sinon @automock/adapters.nestjs
示例
此处提供的示例展示了 Automock 与 Jest 的集成。但是,相同的原则和功能也适用于 Sinon。
考虑以下 CatService
类,它依赖于 Database
类来获取猫。我们将模拟
Database
类以单独测试 CatsService
类。
@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
来创建我们的测试环境。
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)
})
})
在测试设置中,我们:
- 使用
TestBed.create(CatsService).compile()
为CatsService
创建测试环境。 - 分别使用
unit
和unitRef.get(Database)
获取CatsService
的实际实例和Database
的模拟实例。 - 我们模拟
Database
类的getCats
方法以返回预定义的猫列表。 - 然后,我们调用
CatsService
的getAllCats
方法并验证它是否正确与Database
类交互并返回预期的猫。
添加记录器
让我们通过添加 Logger
接口并将其集成到 CatsService
类中来扩展我们的示例。
@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
依赖项:
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
时直接定义模拟行为。
您可以按照以下方法修改测试设置以使用此方法:
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()
方法返回一个具有两个重要属性的对象:unit
和 unitRef
。
这些属性分别提供对被测类实例的访问和对其依赖项的引用。
unit
- unit 属性表示被测类的实际实例。在我们的示例中,它对应于 CatsService
类的一个实例。这允许您在测试场景中直接与该类交互并调用其方法。
unitRef
- unitRef 属性用作对被测类依赖项的引用。在我们的示例中,它引用了 CatsService
使用的 Logger
依赖项。通过访问 unitRef
,您可以检索依赖项的自动生成的模拟对象。这使您能够存根方法、定义行为并在模拟对象上断言方法调用。
使用不同的提供程序
提供程序是 Nest 中最重要的元素之一。您可以将许多默认 Nest 类视为提供程序,包括服务、存储库、工厂、助手等。提供程序的主要功能是采用Injectable
依赖项的形式。
考虑以下CatsService
,它接受一个参数,该参数是以下Logger
接口的一个实例:
export interface Logger {
log: (message: string) => void
}
@Injectable()
export class CatsService {
constructor(private logger: Logger) {}
}
TypeScript 的 Reflection API 尚不支持接口反射。Nest 使用基于字符串/符号的注入令牌解决了这个问题(请参阅 自定义提供程序):
export const MyLoggerProvider = {
provide: 'LOGGER_TOKEN',
useValue: { ... },
}
@Injectable()
export class CatsService {
constructor(@Inject('LOGGER_TOKEN') readonly logger: Logger) {}
}
】
Automock 遵循这种做法,并允许您提供基于字符串(或基于符号)的标记,而不是在 unitRef.get()
方法中提供实际的类:
const { unit, unitRef } = TestBed.create(CatsService).compile()
const loggerMock: jest.Mocked<Logger> = unitRef.get('LOGGER_TOKEN')
更多信息
请访问 Automock GitHub 存储库 或 Automock 网站 了解更多信息。