NestJS 測試

2023-09-08 15:21 更新

自動(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)化測試的特性:

  • 對于組件和應(yīng)用e2e測試的自動(dòng)測試腳手架。
  • 提供默認(rèn)工具(例如test runner構(gòu)建隔離的模塊,應(yīng)用載入器)。
  • 提供JestSuperTest開箱即用的集成。兼容其他測試工具。
  • 在測試環(huán)境中保證Nest依賴注入系統(tǒ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ù)庫。在下一部分中我們會這么做,但也可以在單元測試中這樣使用。

端到端測試(E2E)

與重點(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è)不同的在自定義提供者中描述的方法鏡像:

  • useClass: 提供一個(gè)類來覆蓋對象(提供者,守衛(wèi)等)。
  • useValue: 提供一個(gè)實(shí)例來覆蓋對象。
  • useFactory: 提供一個(gè)方法來返回覆蓋對象的實(shí)例。

每個(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é)尾。

覆蓋全局注冊的強(qiáng)化程序

如果有一個(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。

測試請求范圍實(shí)例

請求范圍提供者針對每個(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);


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號