NestJS 测试
自动化测试是成熟软件产品的重要组成部分。对于覆盖系统中关键的部分是极其重要的。自动化测试使开发过程中的重复独立测试或单元测试变得快捷。这有助于保证发布的质量和性能。在关键开发周期例如源码检入,特征集成和版本管理中使用自动化测试有助于提高覆盖率以及提高开发人员生产力。
测试通常包括不同类型,包括单元测试,端到端(e2e)测试,集成测试等。虽然其优势明显,但是配置往往繁复。Nest 提供了一系列改进测试体验的测试实用程序,包括下列有助于开发者和团队建立自动化测试的特性:
- 对于组件和应用e2e测试的自动测试脚手架。
- 提供默认工具(例如test runner构建隔离的模块,应用载入器)。
- 提供Jest和SuperTest开箱即用的集成。兼容其他测试工具。
- 在测试环境中保证Nest依赖注入系统可用以简化模拟组件。
通常,您可以使用您喜欢的任何测试框架,Nest对此并未强制指定特定工具。简单替换需要的元素(例如test runner),仍然可以享受Nest准备好的测试工具的优势。
安装
首先,我们需要安装所需的 npm 包:
$ npm i --save-dev @nestjs/testing
单元测试
在下面的例子中,我们有两个不同的类,分别是 CatsController 和 CatsService 。如前所述,Jest被用作一个完整的测试框架。该框架是test runner, 并提供断言函数和提升测试实用工具,以帮助 mocking,spying 等。以下示例中,我们手动实例化这些类,并保证控制器和服务满足他们的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);
});
});
});
保持你的测试文件测试类附近。测试文件必须以 .spec 或 .test 结尾
到目前为止,我们没有使用任何现有的 Nest 测试工具。实际上,我们甚至没有使用依赖注入(注意我们把CatsService实例传递给了catsController)。由于我们手动处理实例化测试类,因此上面的测试套件与 Nest 无关。这种类型的测试称为隔离测试。我们接下来介绍一下利用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()方法类似),并返回一个准备用于测试的模块。
compile()方法是异步的,因此必须等待执行完成。一旦模块编译完成,您可以使用 get() 方法获取任何声明的静态实例(控制器和提供者)。
TestingModule继承自module reference类,因此具备动态处理提供者的能力(暂态的或者请求范围的),可以使用resolve() 方法(get()方法尽可以获取静态实例).
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = await moduleRef.resolve(CatsService);
resolve()方法从其自身的注入容器子树返回一个提供者的单例,每个子树都有一个独有的上下文引用。因此,如果你调用这个方法多次,可以看到它们是不同的。
为了模拟一个真实的实例,你可以用自定义的提供者用户提供者覆盖现有的提供者。例如,你可以模拟一个数据库服务来替代连接数据库。在下一部分中我们会这么做,但也可以在单元测试中这样使用。
端到端测试(E2E)
与重点在控制单独模块和类的单元测试不同,端对端测试在更聚合的层面覆盖了类和模块的交互——和生产环境下终端用户类似。当应用程序代码变多时,很难手动测试每个 API 端点的行为。端到端测试帮助我们确保一切工作正常并符合项目要求。为了执行 e2e 测试,我们使用与单元测试相同的配置,但另外我们使用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();
});
});
如果使用Fasify作为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 */)
});
})
在这个例子中,我们使用了之前描述的概念,在之前使用的compile()外,我们使用createNestApplication()方法来实例化一个Nest运行环境。我们在app变量中储存了一个app引用以便模拟HTTP请求。
使用Supertest的request()方法来模拟HTTP请求。我们希望这些HTTP请求访问运行的Nest应用,因此向request()传递一个Nest底层的HTTP监听者(可能由Express平台提供),以此构建请求(app.getHttpServer()),调用request()交给我们一个包装的HTTP服务器以连接Nest应用,它暴露了模拟真实HTTP请求的方法。例如,使用request(...).get('/cats')将初始化一个和真实的从网络来的get '/cats'相同的HTTP请求。
在这个例子中,我们也提供了一个可选的CatsService(test-double)应用,它返回一个硬编码值供我们测试。使用overrideProvider()来进行覆盖替换。类似地,Nest也提供了覆盖守卫,拦截器,过滤器和管道的方法:overrideGuard(), overrideInterceptor(), overrideFilter(), overridePipe()。
每个覆盖方法返回包括3个不同的在自定义提供者中描述的方法镜像:
- useClass: 提供一个类来覆盖对象(提供者,守卫等)。
- useValue: 提供一个实例来覆盖对象。
- useFactory: 提供一个方法来返回覆盖对象的实例。
每个覆盖方法都返回TestingModule实例,可以通过链式写法与其他方法连接。可以在结尾使用compile()方法以使Nest实例化和初始化模块。
The compiled module has several useful methods, as described in the following table: cats.e2e-spec.ts测试文件包含一个 HTTP 端点测试(/cats)。我们使用 app.getHttpServer()方法来获取在 Nest 应用程序的后台运行的底层 HTTP 服务。请注意,TestingModule实例提供了 overrideProvider() 方法,因此我们可以覆盖导入模块声明的现有提供程序。另外,我们可以分别使用相应的方法,overrideGuard(),overrideInterceptor(),overrideFilter()和overridePipe()来相继覆盖守卫,拦截器,过滤器和管道。
编译好的模块有几种在下表中详细描述的方法:
createNestInstance() | 基于给定模块创建一个Nest实例(返回INestApplication ),请注意,必须使用init() 方法手动初始化应用程序 |
createNestMicroservice() | 基于给定模块创建Nest微服务实例(返回INestMicroservice) |
get() | 从module reference 类继承,检索应用程序上下文中可用的控制器或提供程序(包括警卫,过滤器等)的实例 |
resolve() | 从module reference 类继承,检索应用程序上下文中控制器或提供者动态创建的范围实例(包括警卫,过滤器等)的实例 |
select() | 浏览模块树,从所选模块中提取特定实例(与get() 方法中严格模式{strict:true} 一起使用) |
将您的 e2e 测试文件保存在 test 目录下, 并且以 .e2e-spec 或 .e2e-test 结尾。
覆盖全局注册的强化程序
如果有一个全局注册的守卫 (或者管道,拦截器或过滤器),可能需要更多的步骤来覆盖他们。 将原始的注册做如下修改:
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
这样通过APP_*把守卫注册成了“multi”-provider。要在这里替换 JwtAuthGuard`,应该在槽中使用现有提供者。
providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
},
JwtAuthGuard,
],
将useClass修改为useExisting来引用注册提供者,而不是在令牌之后使用Nest实例化。
现在JwtAuthGuard在Nest可以作为一个常规的提供者,也可以在创建TestingModule时被覆盖 :
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useClass(MockAuthGuard)
.compile();
这样测试就会在每个请求中使用MockAuthGuard。
测试请求范围实例
请求范围提供者针对每个请求创建。其实例在请求处理完成后由垃圾回收机制销毁。这产生了一个问题,因为我们无法针对一个测试请求获取其注入依赖子树。
我们知道(基于前节内容),resolve()方法可以用来获取一个动态实例化的类。因此,我们可以传递一个独特的上下文引用来控制注入容器子树的声明周期。如何来在测试上下文中暴露它呢?
策略是生成一个上下文向前引用并且强迫Nest使用这个特殊ID来为所有输入请求创建子树。这样我们就可以获取为测试请求创建的实例。
将jest.spyOn()应用于ContextIdFactory来实现此目的:
const contextId = ContextIdFactory.create();
jest
.spyOn(ContextIdFactory, 'getByRequest')
.mockImplementation(() => contextId);
现在我们可以使用这个contextId来在任何子请求中获取一个生成的注入容器子树。
catsService = await moduleRef.resolve(CatsService, contextId);
更多建议: