Skip to main content

测试

自动化测试被视为任何严肃软件开发工作的重要组成部分。自动化使在开发过程中快速轻松地重复单个测试或测试套件成为可能。 这有助于确保发布达到质量和性能目标。自动化有助于提高测试覆盖率,并为开发人员提供更快的反馈循环。 自动化既提高了个人开发人员的生产力,又确保在关键的开发生命周期节点运行测试,例如源代码控制提交、功能集成和版本发布。

这些测试通常涵盖各种类型,包括单元测试、端到端(e2e)测试、集成测试等。 虽然利益是不可否认的,但设置它们可能会很繁琐。Nest致力于推广开发最佳实践, 包括有效的测试,因此它提供了以下功能,以帮助开发人员和团队构建和自动化测试。Nest:

  • 自动为组件生成默认的单元测试和应用程序的端到端测试
  • 提供默认工具(如构建孤立模块/应用程序加载器的测试运行器)
  • 提供与Jest和Supertest的开箱即用的集成,同时对测试工具保持中立
  • 在测试环境中使用Nest依赖注入系统,轻松模拟组件

正如前面提到的,您可以使用任何您喜欢的测试框架,因为Nest不强制使用特定的工具。 只需替换所需的元素(如测试运行器),您仍然可以享受Nest提供的现成测试设施的好处。

安装

要开始,请首先安装所需的包:

npm i --save-dev @nestjs/testing

单元测试

在以下示例中,我们测试两个类:CatsControllerCatsService。 如前所述,Jest 被提供作为默认的测试框架。 它充当测试运行器,并提供断言函数和测试替身工具,有助于进行模拟、间谍等操作。 在以下基本测试中,我们手动实例化这些类,并确保控制器和服务满足其 API 契约。

cats.controller.spec.ts
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;

beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});

describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

expect(await catsController.findAll()).toBe(result);
});
});
});
tip

保持测试文件靠近它们测试的类。测试文件应该具有.spec.test后缀。

由于上面的示例很简单,我们实际上并没有测试任何与 Nest 相关的内容。 事实上,我们甚至没有使用依赖注入(请注意,我们将CatsService的实例传递给了我们的catsController)。 这种测试形式 - 在其中手动实例化要测试的类 - 通常称为孤立(isolated)测试,因为它独立于框架。 让我们引入一些更先进的功能,帮助您测试更广泛使用 Nest 特性的应用程序。

测试工具

@nestjs/testing 包提供了一组实用程序,支持更强大的测试过程。让我们使用内置的Test类重新编写前面的示例:

cats.controller.spec.ts
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();

catsService = moduleRef.get<CatsService>(CatsService);
catsController = moduleRef.get<CatsController>(CatsController);
});

describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

expect(await catsController.findAll()).toBe(result);
});
});
});

Test类对于提供一个模拟完整 Nest 运行时的应用程序执行上下文非常有用,但同时提供了一些钩子,使得很容易管理类实例,包括模拟和覆盖。 Test类有一个createTestingModule()方法,它以模块元数据对象作为其参数(与您传递给@Module()装饰器的对象相同)。 此方法返回一个TestingModule实例,它反过来提供了一些方法。 对于单元测试,其中一个重要的方法是compile()方法。 此方法使用其依赖项启动模块(类似于在传统的main.ts文件中使用NestFactory.create()启动应用程序), 并返回一个准备好进行测试的模块。

tip

compile()方法是异步的,因此必须等待。 一旦编译了模块,您就可以使用get()方法检索它声明的任何静态实例(控制器和提供者)。

TestingModule继承自模块引用类,因此具有动态解析作用域提供者(瞬态或请求作用域)的能力。 使用resolve()方法实现这一点(get() 方法只能检索静态实例)。

const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();

catsService = await moduleRef.resolve(CatsService);
warning

resolve()方法返回提供者的唯一实例,来自其自身的 DI 容器子树。 每个子树都有一个唯一的上下文标识符。因此,如果调用此方法多次并比较实例引用,则会发现它们不相等。

tip

这里了解更多关于模块引用功能的信息。

您可以使用自定义提供者覆盖任何提供者的生产版本,以进行测试。 例如,您可以模拟数据库服务,而不是连接到实际的数据库。我们将在下一节中介绍覆盖功能,但它们也适用于单元测试。

自动模拟

Nest 还允许您定义一个模拟工厂,应用于所有缺少的依赖项。 这对于类中有大量依赖项的情况很有用,因为模拟所有这些依赖项将花费很长时间并需要大量设置。 要使用此功能,createTestingModule()将需要链接useMocker()方法,传递一个用于依赖项模拟的工厂。 该工厂可以接受一个可选的令牌,该令牌是实例令牌,任何对 Nest 提供程序有效的令牌,并返回一个模拟实现。 下面是使用jest-mock创建通用mocker和使用jest.fn()创建CatsService的特定模拟的示例。

// ...
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';

const moduleMocker = new ModuleMocker(global);

describe('CatsController', () => {
let controller: CatsController;

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
})
.useMocker((token) => {
const results = ['test1', 'test2'];
if (token === CatsService) {
return { findAll: jest.fn().mockResolvedValue(results) };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();

controller = moduleRef.get(CatsController);
});
});

您还可以像通常使用自定义提供者一样从测试容器中检索这些模拟,使moduleRef.get(CatsService)

tip

通用模拟工厂,比如@golevelup/ts-jestcreateMock,也可以直接传递。

tip

REQUESTINQUIRER提供程序无法自动模拟,因为它们已经在上下文中预定义。 但是,它们可以使用自定义提供者语法或通过使用.overrideProvider方法进行覆盖。

端到端测试

与侧重于单个模块和类的单元测试不同,端到端(e2e)测试涵盖了更粗粒度的类和模块的交互 - 更接近最终用户与生产系统交互的方式。 随着应用程序的增长,手动测试每个 API 端点的端到端行为变得困难。 自动化的端到端测试帮助我们确保系统的整体行为是正确的,并满足项目要求。 为了执行 e2e 测试,我们使用与我们刚刚在单元测试中介绍的相似的配置。 此外,Nest 使使用 Supertest 库模拟 HTTP 请求变得轻松。

cats.e2e-spec.ts
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';

describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();

app = moduleRef.createNestApplication();
await app.init();
});

it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});

afterAll(async () => {
await app.close();
});
});
tip

如果您将 Fastify 作为 HTTP 适配器,则需要稍微不同的配置,并且内置了测试功能:

let app: NestFastifyApplication;

beforeAll(async () => {
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());

await app.init();
await app.getHttpAdapter().getInstance().ready();
});

it(`/GET cats`, () => {
return app
.inject({
method:

'GET',
url: '/cats',
})
.then((result) => {
expect(result.statusCode).toEqual(200);
expect(result.payload).toEqual(/* expectedPayload */);
});
});

afterAll(async () => {
await app.close();
});

在这个例子中,我们构建在先前描述的一些概念之上。除了我们之前使用的compile()方法之外, 现在我们使用createNestApplication()方法来实例化完整的 Nest 运行时环境。 我们将正在运行的应用程序的引用保存在我们的app变量中,以便我们可以使用它来模拟 HTTP 请求。

我们使用 Supertest 的request()函数模拟 HTTP 测试。 我们希望这些 HTTP 请求路由到我们运行的 Nest 应用程序, 因此我们将request()函数传递给 Nest 底层的 HTTP 监听器的引用(反过来可能由 Express 平台提供)。 因此构建request(app.getHttpServer())。调用request()会将包装后的 HTTP 服务器返回给我们, 现在连接到 Nest 应用程序,它公开了模拟实际 HTTP 请求的方法。 例如,使用request(...).get('/cats')将启动到 Nest 应用程序的请求,该请求与通过网络进入的实际 HTTP 请求相同。

在这个例子中,我们还提供了CatsService的另一种(测试替身)实现,它只返回我们可以进行测试的硬编码值。 使用overrideProvider()提供这样的替代实现。 同样,Nest 提供了使用overrideModule()overrideGuard()overrideInterceptor()overrideFilter()overridePipe()方法分别覆盖模块、守卫、拦截器、过滤器和管道的方法。

每个override(除了overrideModule())方法类型都返回一个具有 3 种不同方法的对象, 这些方法与上述用于自定义提供者的方法相同:

  • useClass:您提供一个类,该类将被实例化以提供要覆盖的对象的实例(提供程序、守卫等)。
  • useValue:您提供一个实例,该实例将覆盖对象。
  • useFactory:您提供一个返回要覆盖对象的实例的函数。

另一方面,overrideModule()返回一个带有useModule()方法的对象,您可以使用它提供要覆盖原始模块的模块, 如下所示:

const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(CatsModule)
.useModule(AlternateCatsModule)
.compile();

每个override方法类型反过来都返回TestingModule实例,因此可以与流畅样式中的其他方法链接在一起。 在这样的链的末尾应该使用compile(),以使 Nest 实例化和初始化模块。

此外,有时您可能想要在测试运行时(例如,在 CI 服务器上)提供自定义的记录器。 使用setLogger()方法并传递实现LoggerService接口的对象, 以指示TestModuleBuilder 在测试期间如何记录(默认情况下,仅将 "error" 日志记录到控制台)。

编译后的模块有一些有用的方法,如下表所述:

createNestApplication()根据给定的模块创建并返回 Nest 应用程序(INestApplication实例)。请注意,您必须手动使用init()方法初始化应用程序。
createNestMicroservice()根据给定的模块创建并返回 Nest 微服务(INestMicroservice实例)。
get()检索应用程序上下文中可用的控制器或提供者(包括守卫、过滤器等)的静态实例。从模块引用类继承。
resolve()检索应用程序上下文中可用的控制器或提供者(包括守卫、过滤器等)的动态创建的作用域实例(请求或瞬态)。从模块引用类继承。
select()导航模块的依赖关系图;可用于从所选模块中检索特定实例(与 get() 方法一起使用 strict 模式(strict: true))。
tip

将您的 e2e 测试文件放在test目录中。测试文件应具有.e2e-spec后缀。

覆盖全局注册的增强器

如果您有一个全局注册的守卫(或管道、拦截器或过滤器),则需要采取更多步骤来覆盖该增强器。 首先,回顾一下原始的注册如下:

providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],

这是通过APP_*令牌将守卫注册为“multi”提供程序。为了能够替换这里的JwtAuthGuard,注册需要使用此槽位中的现有提供程序:

providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
// ^^^^^^^^ 注意使用 'useExisting' 而不是 'useClass'
},
JwtAuthGuard,
],
tip

useClass更改为useExisting以引用已注册的提供程序,而不是让 Nest 在令牌后面实例化它。

现在,JwtAuthGuard在 Nest 中可见,可以在创建TestingModule时覆盖它:

const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useClass(MockAuthGuard)
.compile();

现在,所有的测试都将在每个请求上使用MockAuthGuard

测试请求作用域的实例

为每个传入请求唯一创建的请求作用域提供程序。该实例在请求完成处理后被垃圾回收。 这带来了一个问题,因为我们无法访问为测试请求生成的依赖注入子树。

我们知道(基于上面的章节)resolve() 方法可用于检索动态实例化的类。 此外,正如在此处所述,我们知道我们可以传递一个唯一的上下文标识符来控制为测试请求创建 DI 容器子树的生命周期。 我们如何在测试上下文中利用这一点?

策略是预先生成一个上下文标识符,并强制 Nest 使用这个特定 ID 来为所有传入请求创建子树。 通过这种方式,我们将能够检索为测试请求创建的实例。

为此,请对ContextIdFactory使用jest.spyOn()

const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, 'getByRequest').mockImplementation(() => contextId);

现在,我们可以使用contextId来访问任何后续请求的单个生成的 DI 容器子树。

catsService = await moduleRef.resolve(CatsService, contextId);