Testing 测试
Automated testing is considered an essential part of any serious software development effort. Automation makes it easy to repeat individual tests or test suites quickly and easily during development. This helps ensure that releases meet quality and performance goals. Automation helps increase coverage and provides a faster feedback loop to developers. Automation both increases the productivity of individual developers and ensures that tests are run at critical development lifecycle junctures, such as source code control check-in, feature integration, and version release.
自动化测试被认为是任何严肃的软件开发工作的重要组成部分。在开发过程中,自动化可以方便快捷地重复单个测试或测试套件。这有助于确保发布的版本符合质量和性能目标。自动化有助于提高覆盖率,并为开发人员提供更快的反馈回路。自动化既能提高单个开发人员的工作效率,又能确保在关键的开发生命周期节点(如源代码控制签入、功能集成和版本发布)运行测试。
Such tests often span a variety of types, including unit tests, end-to-end (e2e) tests, integration tests, and so on. While the benefits are unquestionable, it can be tedious to set them up. Nest strives to promote development best practices, including effective testing, so it includes features such as the following to help developers and teams build and automate tests. Nest:
此类测试通常有多种类型,包括单元测试、端到端(e2e)测试、集成测试等。这些测试的好处毋庸置疑,但设置起来却很繁琐。Nest 致力于推广包括有效测试在内的最佳开发实践,因此它包含了以下功能,以帮助开发人员和团队构建测试并使其自动化。Nest:
- automatically scaffolds default unit tests for components and e2e tests for applications
自动构建组件的默认单元测试和应用程序的 e2e 测试 - provides default tooling (such as a test runner that builds an isolated module/application loader)
提供默认工具(如构建隔离模块/应用程序加载器的测试运行程序) - provides integration with Jest and Supertest out-of-the-box, while remaining agnostic to testing tools
开箱即与 Jest 和 Supertest 集成,同时与测试工具无关 - makes the Nest dependency injection system available in the testing environment for easily mocking components
在测试环境中使用 Nest 依赖注入系统,轻松模拟组件
As mentioned, you can use any testing framework that you like, as Nest doesn't force any specific tooling. Simply replace the elements needed (such as the test runner), and you will still enjoy the benefits of Nest's ready-made testing facilities.
如前所述,您可以使用任何您喜欢的测试框架,因为 Nest 并不强制使用任何特定的工具。只需替换所需的元素(如测试运行器),你就能享受到 Nest 现成测试设施的好处。
Installation# 安装 #
To get started, first install the required package:
要开始使用,首先安装所需的软件包:
$ npm i --save-dev @nestjs/testing
Unit testing# 单元测试 #
In the following example, we test two classes: CatsController
and CatsService
. As mentioned, Jest is provided as the default testing framework. It serves as a test-runner and also provides assert functions and test-double utilities that help with mocking, spying, etc. In the following basic test, we manually instantiate these classes, and ensure that the controller and service fulfill their API contract.
在下面的示例中,我们测试两个类: CatsController
和 CatsService
。如前所述,Jest 是作为默认测试框架提供的。它可作为测试运行器,还提供了 assert 函数和 test-double 实用程序,有助于模拟和监视等。在下面的基本测试中,我们将手动实例化这些类,并确保控制器和服务履行其 API 合同。
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);
});
});
});
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController;
let 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);
});
});
});
Hint 提示 Keep your test files located near the classes they test. Testing files should have a.spec
or.test
suffix.
将测试文件放置在所测试的类附近。测试文件应使用.spec
或.test
后缀。
Because the above sample is trivial, we aren't really testing anything Nest-specific. Indeed, we aren't even using dependency injection (notice that we pass an instance of CatsService
to our catsController
). This form of testing - where we manually instantiate the classes being tested - is often called isolated testing as it is independent from the framework. Let's introduce some more advanced capabilities that help you test applications that make more extensive use of Nest features.
由于上述示例非常琐碎,因此我们并没有测试任何 Nest 特有的内容。事实上,我们甚至没有使用依赖注入(请注意,我们向 catsController
传递了 CatsService
的实例)。这种手动实例化被测类的测试形式通常被称为隔离测试,因为它独立于框架之外。下面我们来介绍一些更高级的功能,它们可以帮助你测试更广泛使用 Nest 功能的应用程序。
Testing utilities# 测试工具 #
The @nestjs/testing
package provides a set of utilities that enable a more robust testing process. Let's rewrite the previous example using the built-in Test
class:
@nestjs/testing
软件包提供了一系列实用程序,使测试过程更加强大。让我们使用内置的 Test
类重写前面的示例:
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);
});
});
});
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController;
let catsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = moduleRef.get(CatsService);
catsController = moduleRef.get(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);
});
});
});
The Test
class is useful for providing an application execution context that essentially mocks the full Nest runtime, but gives you hooks that make it easy to manage class instances, including mocking and overriding. The Test
class has a createTestingModule()
method that takes a module metadata object as its argument (the same object you pass to the @Module()
decorator). This method returns a TestingModule
instance which in turn provides a few methods. For unit tests, the important one is the compile()
method. This method bootstraps a module with its dependencies (similar to the way an application is bootstrapped in the conventional main.ts
file using NestFactory.create()
), and returns a module that is ready for testing.
Test
类可用于提供应用程序执行上下文,该上下文本质上模拟了完整的 Nest 运行时,但为您提供了钩子,使您可以轻松管理类实例,包括模拟和覆盖。 Test
类有一个 createTestingModule()
方法,该方法的参数是模块元数据对象(与你传递给 @Module()
装饰器的对象相同)。该方法会返回一个 TestingModule
实例,而该实例又会提供一些方法。对于单元测试来说,最重要的是 compile()
方法。该方法将一个模块与它的依赖关系一起引导(类似于在传统的 main.ts
文件中使用 NestFactory.create()
引导应用程序的方式),并返回一个可用于测试的模块。
Hint 提示 Thecompile()
method is asynchronous and therefore has to be awaited. Once the module is compiled you can retrieve any static instance it declares (controllers and providers) using theget()
method.
compile()
方法是异步的,因此必须等待。一旦模块编译完成,就可以使用get()
方法检索模块声明的任何静态实例(控制器和提供程序)。
TestingModule
inherits from the module reference class, and therefore its ability to dynamically resolve scoped providers (transient or request-scoped). Do this with the resolve()
method (the get()
method can only retrieve static instances).
TestingModule
继承自模块引用类,因此能够动态解析作用域提供程序(瞬时或请求作用域)。请使用 resolve()
方法实现这一功能( get()
方法只能检索静态实例)。
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = await moduleRef.resolve(CatsService);
Warning 警告 Theresolve()
method returns a unique instance of the provider, from its own DI container sub-tree. Each sub-tree has a unique context identifier. Thus, if you call this method more than once and compare instance references, you will see that they are not equal.
resolve()
方法从其 DI 容器子树返回提供程序的唯一实例。每个子树都有一个唯一的上下文标识符。因此,如果多次调用该方法并比较实例引用,就会发现它们并不相同。
Hint 提示 Learn more about the module reference features here.
点击此处了解有关模块参考功能的更多信息。
Instead of using the production version of any provider, you can override it with a custom provider for testing purposes. For example, you can mock a database service instead of connecting to a live database. We'll cover overrides in the next section, but they're available for unit tests as well.
您可以使用自定义提供程序来覆盖任何提供程序的生产版本,而不是用于测试目的。例如,你可以模拟数据库服务,而不是连接到实时数据库。我们将在下一节介绍覆盖,但它们也可用于单元测试。
Auto mocking# 自动模拟 #
Nest also allows you to define a mock factory to apply to all of your missing dependencies. This is useful for cases where you have a large number of dependencies in a class and mocking all of them will take a long time and a lot of setup. To make use of this feature, the createTestingModule()
will need to be chained up with the useMocker()
method, passing a factory for your dependency mocks. This factory can take in an optional token, which is an instance token, any token which is valid for a Nest provider, and returns a mock implementation. The below is an example of creating a generic mocker using jest-mock
and a specific mock for CatsService
using jest.fn()
.
Nest 还允许你定义一个模拟工厂,以应用于所有缺失的依赖项。这对于在一个类中有大量依赖项,而模拟所有依赖项需要很长时间和大量设置的情况非常有用。要使用此功能,需要将 createTestingModule()
与 useMocker()
方法串联起来,并为依赖关系模拟传递一个工厂。该工厂可以接收一个可选的令牌(即实例令牌),任何对 Nest 提供者有效的令牌,并返回一个模拟实现。下面是使用 jest-mock
创建通用模拟器和使用 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);
});
});
You can also retrieve these mocks out of the testing container as you normally would custom providers, moduleRef.get(CatsService)
.
Hint A general mock factory, likecreateMock
from@golevelup/ts-jest
can also be passed directly.
HintREQUEST
andINQUIRER
providers cannot be auto-mocked because they're already pre-defined in the context. However, they can be overwritten using the custom provider syntax or by utilizing the.overrideProvider
method.
End-to-end testing#
Unlike unit testing, which focuses on individual modules and classes, end-to-end (e2e) testing covers the interaction of classes and modules at a more aggregate level -- closer to the kind of interaction that end-users will have with the production system. As an application grows, it becomes hard to manually test the end-to-end behavior of each API endpoint. Automated end-to-end tests help us ensure that the overall behavior of the system is correct and meets project requirements. To perform e2e tests we use a similar configuration to the one we just covered in unit testing. In addition, Nest makes it easy to use the Supertest library to simulate HTTP requests.
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();
});
});
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();
});
});
Hint If you're using Fastify as your HTTP adapter, it requires a slightly different configuration, and has built-in testing capabilities: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(); });
In this example, we build on some of the concepts described earlier. In addition to the compile()
method we used earlier, we now use the createNestApplication()
method to instantiate a full Nest runtime environment. We save a reference to the running app in our app
variable so we can use it to simulate HTTP requests.
We simulate HTTP tests using the request()
function from Supertest. We want these HTTP requests to route to our running Nest app, so we pass the request()
function a reference to the HTTP listener that underlies Nest (which, in turn, may be provided by the Express platform). Hence the construction request(app.getHttpServer())
. The call to request()
hands us a wrapped HTTP Server, now connected to the Nest app, which exposes methods to simulate an actual HTTP request. For example, using request(...).get('/cats')
will initiate a request to the Nest app that is identical to an actual HTTP request like get '/cats'
coming in over the network.
In this example, we also provide an alternate (test-double) implementation of the CatsService
which simply returns a hard-coded value that we can test for. Use overrideProvider()
to provide such an alternate implementation. Similarly, Nest provides methods to override modules, guards, interceptors, filters and pipes with the overrideModule()
, overrideGuard()
, overrideInterceptor()
, overrideFilter()
, and overridePipe()
methods respectively.
Each of the override methods (except for overrideModule()
) returns an object with 3 different methods that mirror those described for custom providers:
useClass
: you supply a class that will be instantiated to provide the instance to override the object (provider, guard, etc.).useValue
: you supply an instance that will override the object.useFactory
: you supply a function that returns an instance that will override the object.
On the other hand, overrideModule()
returns an object with the useModule()
method, which you can use to supply a module that will override the original module, as follows:
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(CatsModule)
.useModule(AlternateCatsModule)
.compile();
Each of the override method types, in turn, returns the TestingModule
instance, and can thus be chained with other methods in the fluent style. You should use compile()
at the end of such a chain to cause Nest to instantiate and initialize the module.
Also, sometimes you may want to provide a custom logger e.g. when the tests are run (for example, on a CI server). Use the setLogger()
method and pass an object that fulfills the LoggerService
interface to instruct the TestModuleBuilder
how to log during tests (by default, only "error" logs will be logged to the console).
The compiled module has several useful methods, as described in the following table:
createNestApplication() | Creates and returns a Nest application (INestApplication instance) based on the given module. Note that you must manually initialize the application using the init() method. |
createNestMicroservice() | Creates and returns a Nest microservice (INestMicroservice instance) based on the given module. |
get() | Retrieves a static instance of a controller or provider (including guards, filters, etc.) available in the application context. Inherited from the module reference class. |
resolve() | Retrieves a dynamically created scoped instance (request or transient) of a controller or provider (including guards, filters, etc.) available in the application context. Inherited from the module reference class. |
select() | Navigates through the module's dependency graph; can be used to retrieve a specific instance from the selected module (used along with strict mode (strict: true ) in get() method). |
Hint Keep your e2e test files inside thetest
directory. The testing files should have a.e2e-spec
suffix.
Overriding globally registered enhancers#
If you have a globally registered guard (or pipe, interceptor, or filter), you need to take a few more steps to override that enhancer. To recap the original registration looks like this:
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
This is registering the guard as a "multi"-provider through the APP_*
token. To be able to replace the JwtAuthGuard
here, the registration needs to use an existing provider in this slot:
providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
// ^^^^^^^^ notice the use of 'useExisting' instead of 'useClass'
},
JwtAuthGuard,
],
Hint Change theuseClass
touseExisting
to reference a registered provider instead of having Nest instantiate it behind the token.
Now the JwtAuthGuard
is visible to Nest as a regular provider that can be overridden when creating the TestingModule
:
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useClass(MockAuthGuard)
.compile();
Now all your tests will use the MockAuthGuard
on every request.
Testing request-scoped instances#
Request-scoped providers are created uniquely for each incoming request. The instance is garbage-collected after the request has completed processing. This poses a problem, because we can't access a dependency injection sub-tree generated specifically for a tested request.
We know (based on the sections above) that the resolve()
method can be used to retrieve a dynamically instantiated class. Also, as described here, we know we can pass a unique context identifier to control the lifecycle of a DI container sub-tree. How do we leverage this in a testing context?
The strategy is to generate a context identifier beforehand and force Nest to use this particular ID to create a sub-tree for all incoming requests. In this way we'll be able to retrieve instances created for a tested request.
To accomplish this, use jest.spyOn()
on the ContextIdFactory
:
const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, 'getByRequest').mockImplementation(() => contextId);
Now we can use the contextId
to access a single generated DI container sub-tree for any subsequent request.
catsService = await moduleRef.resolve(CatsService, contextId);