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