## 測試
自動化測試是任何成熟**軟件產品**的重要組成部分。對于覆蓋系統中關鍵的部分是極其重要的。自動化測試使開發過程中的重復獨立測試或單元測試變得快捷。這有助于保證發布的質量和性能。在關鍵開發周期例如源碼檢入,特征集成和版本管理中使用自動化測試有助于提高覆蓋率以及提高開發人員生產力。
測試通常包括不同類型,包括單元測試,端到端(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);
```
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?