企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 测试 自动化测试是任何成熟**软件产品**的重要组成部分。对于覆盖系统中关键的部分是极其重要的。自动化测试使开发过程中的重复独立测试或单元测试变得快捷。这有助于保证发布的质量和性能。在关键开发周期例如源码检入,特征集成和版本管理中使用自动化测试有助于提高覆盖率以及提高开发人员生产力。 测试通常包括不同类型,包括单元测试,端到端(e2e)测试,集成测试等。虽然其优势明显,但是配置往往繁琐。`Nest` 提供了一系列改进测试体验的测试实用程序,包括下列有助于开发者和团队建立自动化测试的特性: - 对于组件和应用e2e测试的自动测试脚手架。 - 提供默认工具(例如`test runner`构建隔离的模块,应用载入器)。 - 提供[Jest](https://github.com/facebook/jest)和[SuperTest](https://github.com/visionmedia/supertest)开箱即用的集成。兼容其他测试工具。 - 在测试环境中保证Nest依赖注入系统可用以简化模拟组件。 通常,您可以使用您喜欢的任何**测试框架**,Nest对此并未强制指定特定工具。简单替换需要的元素(例如`test runner`),仍然可以享受Nest准备好的测试工具的优势。 ### 安装 首先,我们需要安装所需的 `npm` 包: ```bash $ npm i --save-dev @nestjs/testing ``` ### 单元测试 在下面的例子中,我们有两个不同的类,分别是 `CatsController` 和 `CatsService` 。如前所述,[Jest](https://github.com/facebook/jest)作为默认测试框架提供。它充当测试运行器,并提供断言函数和提升测试实用工具,以帮助 `模拟(mocking)`,`监听(spying)` 等。以下示例中,我们手动实例化这些类,并保证控制器和服务满足他们的API接口。 > cats.controller.spec.ts ```typescript 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 ```typescript 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](https://docs.nestjs.com/fundamentals/module-ref)类,因此具备动态处理提供者的能力(暂态的或者请求范围的),可以使用`resolve() `方法(`get()`方法只能检索获取静态实例). ```typescript const moduleRef = await Test.createTestingModule({ controllers: [CatsController], providers: [CatsService], }).compile(); catsService = await moduleRef.resolve(CatsService); ``` > `resolve()`方法从其自身的注入容器子树返回一个提供者的单例,每个子树都有一个独有的上下文引用。因此,如果你调用这个方法多次,可以看到它们是不同的。 为了模拟一个真实的实例,你可以用自定义的提供者[用户提供者](https://docs.nestjs.com/fundamentals/custom-providers)覆盖现有的提供者。例如,你可以模拟一个数据库服务来替代连接数据库,而不是连接到实时数据库。在下一部分中我们会这么做,但也可以在单元测试中这样使用。 ### 自动模拟[#](#auto-mocking) Nest 还允许您定义一个模拟工厂以应用于所有缺少的依赖项。这对于您在一个类中有大量依赖项并且模拟所有依赖项需要很长时间和大量设置的情况很有用。要使用此功能,`createTestingModule()`需要将方法与`useMocker()`方法链接起来,为您的依赖模拟传递一个工厂。这个工厂可以接受一个可选的令牌,它是一个实例令牌,任何对 Nest 提供者有效的令牌,并返回一个模拟实现。下面是创建通用模拟程序使用[`jest-mock`](https://www.npmjs.com/package/jest-mock)和特定模拟程序`CatsService`使用的示例`jest.fn()`。 ~~~typescript const moduleMocker = new ModuleMocker(global); describe('CatsController', () => { let controller: CatsController; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ controllers: [CatsController], }) .useMocker((token) => { 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); }); }) ~~~ > **提示**:一般的`mock factory`,像`createMock`from[`@golevelup/ts-jest`](https://github.com/golevelup/nestjs/tree/master/packages/testing)也可以直接传递。 您还可以像通常自定义提供程序一样从测试容器中检索这些模拟,`moduleRef.get(CatsService)`. ### 端到端测试(E2E) 与重点在控制单独模块和类的单元测试不同,端对端测试在更聚合的层面覆盖了类和模块的交互——和生产环境下终端用户类似。当应用程序代码变多时,很难手动测试每个 `API` 端点的行为。端到端测试帮助我们确保一切工作正常并符合项目要求。为了执行 `e2e` 测试,我们使用与**单元测试**相同的配置,但另外我们使用[supertest](https://github.com/visionmedia/supertest)模拟 `HTTP` 请求。 >cats.e2e-spec.ts ```typescript 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服务器,在配置上有所不同,其有一些内置功能: ```typescript 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` 结尾。 ### 覆盖全局注册的强化程序 如果有一个全局注册的守卫 (或者管道,拦截器或过滤器),可能需要更多的步骤来覆盖他们。 将原始的注册做如下修改: ```typescript providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard, }, ], ``` 这样通过`APP_*`把守卫注册成了`"multi"-provider`。要在这里替换 `JwtAuthGuard`,应该在槽中使用现有提供者。 ```typescript providers: [ { provide: APP_GUARD, useExisting: JwtAuthGuard, // ^^^^^^^^ notice the use of 'useExisting' instead of 'useClass' }, JwtAuthGuard, ], ``` > 将`useClass`修改为`useExisting`来引用注册提供者,而不是在令牌之后使用Nest实例化。 现在`JwtAuthGuard`在Nest可以作为一个常规的提供者,也可以在创建`TestingModule`时被覆盖 : ```typescript const moduleRef = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(JwtAuthGuard) .useClass(MockAuthGuard) .compile(); ``` 这样测试就会在每个请求中使用`MockAuthGuard`。 ### 测试请求范围实例 请求范围提供者针对每个请求创建。其实例在请求处理完成后由垃圾回收机制销毁。这产生了一个问题,因为我们无法针对一个测试请求获取其注入依赖子树。 我们知道(基于前节内容),`resolve()`方法可以用来获取一个动态实例化的类。因此,我们可以传递一个独特的上下文引用来控制注入容器子树的声明周期。如何来在测试上下文中暴露它呢? 策略是生成一个上下文向前引用并且强迫Nest使用这个特殊ID来为所有输入请求创建子树。这样我们就可以获取为测试请求创建的实例。 将`jest.spyOn()`应用于`ContextIdFactory`来实现此目的: ```typescript const contextId = ContextIdFactory.create(); jest .spyOn(ContextIdFactory, 'getByRequest') .mockImplementation(() => contextId); ``` 现在我们可以使用这个`contextId`来在任何子请求中获取一个生成的注入容器子树。 ```typescript catsService = await moduleRef.resolve(CatsService, contextId); ```