自動(dòng)化測(cè)試是成熟軟件產(chǎn)品的重要組成部分。對(duì)于覆蓋系統(tǒng)中關(guān)鍵的部分是極其重要的。自動(dòng)化測(cè)試使開(kāi)發(fā)過(guò)程中的重復(fù)獨(dú)立測(cè)試或單元測(cè)試變得快捷。這有助于保證發(fā)布的質(zhì)量和性能。在關(guān)鍵開(kāi)發(fā)周期例如源碼檢入,特征集成和版本管理中使用自動(dòng)化測(cè)試有助于提高覆蓋率以及提高開(kāi)發(fā)人員生產(chǎn)力。
測(cè)試通常包括不同類(lèi)型,包括單元測(cè)試,端到端(e2e)測(cè)試,集成測(cè)試等。雖然其優(yōu)勢(shì)明顯,但是配置往往繁復(fù)。Nest 提供了一系列改進(jìn)測(cè)試體驗(yàn)的測(cè)試實(shí)用程序,包括下列有助于開(kāi)發(fā)者和團(tuán)隊(duì)建立自動(dòng)化測(cè)試的特性:
通常,您可以使用您喜歡的任何測(cè)試框架,Nest對(duì)此并未強(qiáng)制指定特定工具。簡(jiǎn)單替換需要的元素(例如test runner),仍然可以享受Nest準(zhǔn)備好的測(cè)試工具的優(yōu)勢(shì)。
首先,我們需要安裝所需的 npm 包:
$ npm i --save-dev @nestjs/testing
在下面的例子中,我們有兩個(gè)不同的類(lèi),分別是 CatsController 和 CatsService 。如前所述,Jest被用作一個(gè)完整的測(cè)試框架。該框架是test runner, 并提供斷言函數(shù)和提升測(cè)試實(shí)用工具,以幫助 mocking,spying 等。以下示例中,我們手動(dòng)實(shí)例化這些類(lèi),并保證控制器和服務(wù)滿(mǎn)足他們的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);
});
});
});
保持你的測(cè)試文件測(cè)試類(lèi)附近。測(cè)試文件必須以 .spec 或 .test 結(jié)尾
到目前為止,我們沒(méi)有使用任何現(xiàn)有的 Nest 測(cè)試工具。實(shí)際上,我們甚至沒(méi)有使用依賴(lài)注入(注意我們把CatsService實(shí)例傳遞給了catsController)。由于我們手動(dòng)處理實(shí)例化測(cè)試類(lèi),因此上面的測(cè)試套件與 Nest 無(wú)關(guān)。這種類(lèi)型的測(cè)試稱(chēng)為隔離測(cè)試。我們接下來(lái)介紹一下利用Nest功能提供的更先進(jìn)的測(cè)試應(yīng)用。
@nestjs/testing 包給了我們一套提升測(cè)試過(guò)程的實(shí)用工具。讓我們重寫(xiě)前面的例子,但現(xiàn)在使用內(nèi)置的 Test 類(lèi)。
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類(lèi)提供應(yīng)用上下文以模擬整個(gè)Nest運(yùn)行時(shí),這一點(diǎn)很有用。 Test 類(lèi)有一個(gè) createTestingModule() 方法,該方法將模塊的元數(shù)據(jù)(與在 @Module() 裝飾器中傳遞的對(duì)象相同的對(duì)象)作為參數(shù)。這個(gè)方法創(chuàng)建了一個(gè) TestingModule 實(shí)例,該實(shí)例提供了一些方法,但是當(dāng)涉及到單元測(cè)試時(shí),這些方法中只有 compile() 是有用的。這個(gè)方法初始化一個(gè)模塊和它的依賴(lài)(和傳統(tǒng)應(yīng)用中從main.ts文件使用NestFactory.create()方法類(lèi)似),并返回一個(gè)準(zhǔn)備用于測(cè)試的模塊。
compile()方法是異步的,因此必須等待執(zhí)行完成。一旦模塊編譯完成,您可以使用 get() 方法獲取任何聲明的靜態(tài)實(shí)例(控制器和提供者)。
TestingModule繼承自module reference類(lèi),因此具備動(dòng)態(tài)處理提供者的能力(暫態(tài)的或者請(qǐng)求范圍的),可以使用resolve() 方法(get()方法盡可以獲取靜態(tài)實(shí)例).
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = await moduleRef.resolve(CatsService);
resolve()方法從其自身的注入容器子樹(shù)返回一個(gè)提供者的單例,每個(gè)子樹(shù)都有一個(gè)獨(dú)有的上下文引用。因此,如果你調(diào)用這個(gè)方法多次,可以看到它們是不同的。
為了模擬一個(gè)真實(shí)的實(shí)例,你可以用自定義的提供者用戶(hù)提供者覆蓋現(xiàn)有的提供者。例如,你可以模擬一個(gè)數(shù)據(jù)庫(kù)服務(wù)來(lái)替代連接數(shù)據(jù)庫(kù)。在下一部分中我們會(huì)這么做,但也可以在單元測(cè)試中這樣使用。
與重點(diǎn)在控制單獨(dú)模塊和類(lèi)的單元測(cè)試不同,端對(duì)端測(cè)試在更聚合的層面覆蓋了類(lèi)和模塊的交互——和生產(chǎn)環(huán)境下終端用戶(hù)類(lèi)似。當(dāng)應(yīng)用程序代碼變多時(shí),很難手動(dòng)測(cè)試每個(gè) API 端點(diǎn)的行為。端到端測(cè)試幫助我們確保一切工作正常并符合項(xiàng)目要求。為了執(zhí)行 e2e 測(cè)試,我們使用與單元測(cè)試相同的配置,但另外我們使用supertest模擬 HTTP 請(qǐng)求。
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服務(wù)器,在配置上有所不同,其有一些內(nèi)置功能:
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 */)
});
})
在這個(gè)例子中,我們使用了之前描述的概念,在之前使用的compile()外,我們使用createNestApplication()方法來(lái)實(shí)例化一個(gè)Nest運(yùn)行環(huán)境。我們?cè)赼pp變量中儲(chǔ)存了一個(gè)app引用以便模擬HTTP請(qǐng)求。
使用Supertest的request()方法來(lái)模擬HTTP請(qǐng)求。我們希望這些HTTP請(qǐng)求訪(fǎng)問(wèn)運(yùn)行的Nest應(yīng)用,因此向request()傳遞一個(gè)Nest底層的HTTP監(jiān)聽(tīng)者(可能由Express平臺(tái)提供),以此構(gòu)建請(qǐng)求(app.getHttpServer()),調(diào)用request()交給我們一個(gè)包裝的HTTP服務(wù)器以連接Nest應(yīng)用,它暴露了模擬真實(shí)HTTP請(qǐng)求的方法。例如,使用request(...).get('/cats')將初始化一個(gè)和真實(shí)的從網(wǎng)絡(luò)來(lái)的get '/cats'相同的HTTP請(qǐng)求。
在這個(gè)例子中,我們也提供了一個(gè)可選的CatsService(test-double)應(yīng)用,它返回一個(gè)硬編碼值供我們測(cè)試。使用overrideProvider()來(lái)進(jìn)行覆蓋替換。類(lèi)似地,Nest也提供了覆蓋守衛(wèi),攔截器,過(guò)濾器和管道的方法:overrideGuard(), overrideInterceptor(), overrideFilter(), overridePipe()。
每個(gè)覆蓋方法返回包括3個(gè)不同的在自定義提供者中描述的方法鏡像:
每個(gè)覆蓋方法都返回TestingModule實(shí)例,可以通過(guò)鏈?zhǔn)綄?xiě)法與其他方法連接。可以在結(jié)尾使用compile()方法以使Nest實(shí)例化和初始化模塊。
The compiled module has several useful methods, as described in the following table: cats.e2e-spec.ts測(cè)試文件包含一個(gè) HTTP 端點(diǎn)測(cè)試(/cats)。我們使用 app.getHttpServer()方法來(lái)獲取在 Nest 應(yīng)用程序的后臺(tái)運(yùn)行的底層 HTTP 服務(wù)。請(qǐng)注意,TestingModule實(shí)例提供了 overrideProvider() 方法,因此我們可以覆蓋導(dǎo)入模塊聲明的現(xiàn)有提供程序。另外,我們可以分別使用相應(yīng)的方法,overrideGuard(),overrideInterceptor(),overrideFilter()和overridePipe()來(lái)相繼覆蓋守衛(wèi),攔截器,過(guò)濾器和管道。
編譯好的模塊有幾種在下表中詳細(xì)描述的方法:
createNestInstance() | 基于給定模塊創(chuàng)建一個(gè)Nest實(shí)例(返回INestApplication ),請(qǐng)注意,必須使用init() 方法手動(dòng)初始化應(yīng)用程序 |
createNestMicroservice() | 基于給定模塊創(chuàng)建Nest微服務(wù)實(shí)例(返回INestMicroservice) |
get() | 從module reference 類(lèi)繼承,檢索應(yīng)用程序上下文中可用的控制器或提供程序(包括警衛(wèi),過(guò)濾器等)的實(shí)例 |
resolve() | 從module reference 類(lèi)繼承,檢索應(yīng)用程序上下文中控制器或提供者動(dòng)態(tài)創(chuàng)建的范圍實(shí)例(包括警衛(wèi),過(guò)濾器等)的實(shí)例 |
select() | 瀏覽模塊樹(shù),從所選模塊中提取特定實(shí)例(與get() 方法中嚴(yán)格模式{strict:true} 一起使用) |
將您的 e2e 測(cè)試文件保存在 test 目錄下, 并且以 .e2e-spec 或 .e2e-test 結(jié)尾。
如果有一個(gè)全局注冊(cè)的守衛(wèi) (或者管道,攔截器或過(guò)濾器),可能需要更多的步驟來(lái)覆蓋他們。 將原始的注冊(cè)做如下修改:
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
這樣通過(guò)APP_*把守衛(wèi)注冊(cè)成了“multi”-provider。要在這里替換 JwtAuthGuard`,應(yīng)該在槽中使用現(xiàn)有提供者。
providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
},
JwtAuthGuard,
],
將useClass修改為useExisting來(lái)引用注冊(cè)提供者,而不是在令牌之后使用Nest實(shí)例化。
現(xiàn)在JwtAuthGuard在Nest可以作為一個(gè)常規(guī)的提供者,也可以在創(chuàng)建TestingModule時(shí)被覆蓋 :
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useClass(MockAuthGuard)
.compile();
這樣測(cè)試就會(huì)在每個(gè)請(qǐng)求中使用MockAuthGuard。
請(qǐng)求范圍提供者針對(duì)每個(gè)請(qǐng)求創(chuàng)建。其實(shí)例在請(qǐng)求處理完成后由垃圾回收機(jī)制銷(xiāo)毀。這產(chǎn)生了一個(gè)問(wèn)題,因?yàn)槲覀儫o(wú)法針對(duì)一個(gè)測(cè)試請(qǐng)求獲取其注入依賴(lài)子樹(shù)。
我們知道(基于前節(jié)內(nèi)容),resolve()方法可以用來(lái)獲取一個(gè)動(dòng)態(tài)實(shí)例化的類(lèi)。因此,我們可以傳遞一個(gè)獨(dú)特的上下文引用來(lái)控制注入容器子樹(shù)的聲明周期。如何來(lái)在測(cè)試上下文中暴露它呢?
策略是生成一個(gè)上下文向前引用并且強(qiáng)迫Nest使用這個(gè)特殊ID來(lái)為所有輸入請(qǐng)求創(chuàng)建子樹(shù)。這樣我們就可以獲取為測(cè)試請(qǐng)求創(chuàng)建的實(shí)例。
將jest.spyOn()應(yīng)用于ContextIdFactory來(lái)實(shí)現(xiàn)此目的:
const contextId = ContextIdFactory.create();
jest
.spyOn(ContextIdFactory, 'getByRequest')
.mockImplementation(() => contextId);
現(xiàn)在我們可以使用這個(gè)contextId來(lái)在任何子請(qǐng)求中獲取一個(gè)生成的注入容器子樹(shù)。
catsService = await moduleRef.resolve(CatsService, contextId);
更多建議: