Asosiy tushunchalar12 min read

Testing

Avtomatlashtirilgan testlash har qanday jiddiy dasturiy ta'minot ishlab chiqishining muhim qismi hisoblanadi. Avtomatlashtirish ishlab chiqish jarayonida alohida testlarni yoki tes

Avtomatlashtirilgan testlash har qanday jiddiy dasturiy ta'minot ishlab chiqishining muhim qismi hisoblanadi. Avtomatlashtirish ishlab chiqish jarayonida alohida testlarni yoki test to'plamlarini tez va oson takrorlash imkonini beradi. Bu relizlar sifat va performance maqsadlariga javob berishini ta'minlashga yordam beradi. Avtomatlashtirish qamrovni oshiradi va dasturchilarga tezroq feedback loop taqdim etadi. Avtomatlashtirish alohida dasturchilar unumdorligini oshiradi hamda testlar manba kodini versiya nazoratiga kiritish, funksiyalarni integratsiya qilish va versiya relizi kabi muhim lifecycle nuqtalarida ishga tushirilishini ta'minlaydi.

Bunday testlar odatda turli turlarni qamrab oladi, jumladan unit testlar, end-to-end (e2e) testlar, integratsion testlar va hokazo. Foydalari shubhasiz bo'lsa-da, ularni sozlash zerikarli bo'lishi mumkin. Nest rivojlanishdagi eng yaxshi amaliyotlarni, jumladan samarali testlashni targ'ib qiladi, shuning uchun u dasturchilar va jamoalarga testlarni qurish va avtomatlashtirishda yordam beruvchi quyidagi imkoniyatlarni taqdim etadi. Nest:

  • komponentlar uchun default unit testlar va ilovalar uchun e2e testlarni avtomatik scaffold qiladi
  • default tooling (masalan, izolyatsiyalangan modul/ilova loaderini quradigan test runner) taqdim etadi
  • Jest va Supertest bilan out-of-the-box integratsiya beradi, testlash vositalari bo'yicha agnostik bo'lib qoladi
  • Nest dependency injection tizimini test muhitida ham taqdim etadi, komponentlarni oson mock qilish uchun

Aytilganidek, siz xohlagan testing framework dan foydalanishingiz mumkin, chunki Nest hech qanday maxsus vositani majburlamaydi. Kerakli elementlarni (masalan, test runner) almashtiring, va siz baribir Nestning tayyor testlash imkoniyatlaridan foydalanasiz.

O'rnatish

Boshlash uchun, avval kerakli paketni o'rnating:

Terminal
1$ npm i --save-dev @nestjs/testing

Unit testlash

Quyidagi misolda biz ikki sinfni test qilamiz: CatsController va CatsService. Aytilganidek, Jest default testing framework sifatida taqdim etiladi. U test-runner bo'lib xizmat qiladi va mocking, spying va hokazo uchun assert funksiyalari hamda test-double utilitalarini taqdim etadi. Quyidagi sodda testda biz bu sinflarni qo'lda instansiyalaymiz va controller hamda service o'z API kontraktini bajarayotganini tekshiramiz.

TypeScript
cats.controller.spec
1import { CatsController } from './cats.controller';
2import { CatsService } from './cats.service';
3
4describe('CatsController', () => {
5  let catsController: CatsController;
6  let catsService: CatsService;
7
8  beforeEach(() => {
9    catsService = new CatsService();
10    catsController = new CatsController(catsService);
11  });
12
13  describe('findAll', () => {
14    it('should return an array of cats', async () => {
15      const result = ['test'];
16      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
17
18      expect(await catsController.findAll()).toBe(result);
19    });
20  });
21});
Hint

Test fayllarini test qilinayotgan sinflarga yaqin joylashtiring. Test fayllari .spec yoki .test suffiksiga ega bo'lishi kerak.

Yuqoridagi namuna trivial bo'lgani uchun, biz Nestga xos narsalarni deyarli test qilmayapmiz. Aslida, biz hatto dependency injectiondan ham foydalanmayapmiz (e'tibor bering, CatsService instansiyasini catsController ga uzatyapmiz). Bunday testlash shakli - test qilinayotgan sinflarni qo'lda instansiyalash - ko'pincha isolated testing deb ataladi, chunki u freymvorkdan mustaqil. Endi Nest imkoniyatlaridan kengroq foydalanadigan ilovalarni testlashda yordam beruvchi biroz ilg'or imkoniyatlarni ko'rib chiqamiz.

Testing utilitalari

@nestjs/testing paketi yanada mustahkam test jarayonini ta'minlaydigan utilitalar to'plamini taqdim etadi. Oldingi misolni o'rnatilgan Test sinfi yordamida qayta yozaylik:

TypeScript
cats.controller.spec
1import { Test } from '@nestjs/testing';
2import { CatsController } from './cats.controller';
3import { CatsService } from './cats.service';
4
5describe('CatsController', () => {
6  let catsController: CatsController;
7  let catsService: CatsService;
8
9  beforeEach(async () => {
10    const moduleRef = await Test.createTestingModule({
11        controllers: [CatsController],
12        providers: [CatsService],
13      }).compile();
14
15    catsService = moduleRef.get(CatsService);
16    catsController = moduleRef.get(CatsController);
17  });
18
19  describe('findAll', () => {
20    it('should return an array of cats', async () => {
21      const result = ['test'];
22      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
23
24      expect(await catsController.findAll()).toBe(result);
25    });
26  });
27});

Test sinfi to'liq Nest runtime'ini mocking qiladigan ilova execution contextini taqdim etishda foydalidir, lekin sinf instansiyalarini boshqarish, mock qilish va override qilish uchun qulay hooklarni beradi. Test sinfi createTestingModule() metodiga ega bo'lib, u modul metadata obyektini argument sifatida qabul qiladi (bu @Module() dekoratoriga uzatadigan obyekt bilan bir xil). Bu metod TestingModule instansiyasini qaytaradi, u esa bir nechta metodlarni taqdim etadi. Unit testlar uchun eng muhim metod - compile(). Bu metod modulni bog'liqliklari bilan birga bootstrap qiladi (xuddi odatdagi main.ts faylida NestFactory.create() bilan ilovani bootstrapping qilgandek) va testlashga tayyor modulni qaytaradi.

Hint

compile() metodi asinxron, shuning uchun uni await qilish kerak. Modul kompilyatsiya qilingach, get() metodi orqali u e'lon qilgan istalgan statik instansiyani (controllerlar va provayderlar) olishingiz mumkin.

TestingModule module reference sinfidan meros oladi va shuning uchun scoped provayderlarni (transient yoki request-scoped) dinamik yechish imkoniyatiga ega. Buni resolve() metodi bilan bajaring (get() metodi faqat statik instansiyalarni oladi).

TypeScript
1const moduleRef = await Test.createTestingModule({
2  controllers: [CatsController],
3  providers: [CatsService],
4}).compile();
5
6catsService = await moduleRef.resolve(CatsService);
Warning

resolve() metodi provayderning noyob instansiyasini, uning DI container sub-tree sidan qaytaradi. Har bir sub-tree uchun noyob context identifier bor. Shuning uchun bu metodni bir necha marta chaqirib, instansiya havolalarini solishtirsangiz, ular teng emasligini ko'rasiz.

Hint

Modulga murojaat imkoniyatlari haqida batafsil bu yerda o'qing.

Ishlab chiqarishdagi provayder o'rniga, test uchun custom provider bilan uni override qilishingiz mumkin. Masalan, live bazaga ulanmasdan, ma'lumotlar bazasi servisini mock qilishingiz mumkin. Override'lar keyingi bo'limda ko'rib chiqiladi, ammo ular unit testlar uchun ham mavjud.

Auto mocking

Nest shuningdek yetishmayotgan barcha bog'liqliklarga qo'llanadigan mock factory aniqlash imkonini beradi. Bu katta sonli bog'liqliklarga ega sinflarda ularning barchasini mock qilish ko'p vaqt va ko'p sozlash talab qiladigan holatlar uchun foydali. Bu imkoniyatdan foydalanish uchun createTestingModule() chaqiruvi useMocker() metodi bilan zanjirlanadi va bog'liqlik mocklari uchun factory uzatiladi. Bu factory ixtiyoriy token qabul qilishi mumkin; token - instansiya tokeni bo'lib, Nest provayderi uchun yaroqli bo'lgan istalgan token bo'lishi mumkin, va u mock implementatsiyani qaytaradi. Quyida jest-mock yordamida umumiy mocker va CatsService uchun jest.fn() yordamida maxsus mock yaratish misoli keltirilgan.

TypeScript
1// ...
2import { ModuleMocker, MockMetadata } from 'jest-mock';
3
4const moduleMocker = new ModuleMocker(global);
5
6describe('CatsController', () => {
7  let controller: CatsController;
8
9  beforeEach(async () => {
10    const moduleRef = await Test.createTestingModule({
11      controllers: [CatsController],
12    })
13      .useMocker((token) => {
14        const results = ['test1', 'test2'];
15        if (token === CatsService) {
16          return { findAll: jest.fn().mockResolvedValue(results) };
17        }
18        if (typeof token === 'function') {
19          const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<
20            any,
21            any
22          >;
23          const Mock = moduleMocker.generateFromMetadata(
24            mockMetadata,
25          ) as ObjectConstructor;
26          return new Mock();
27        }
28      })
29      .compile();
30
31    controller = moduleRef.get(CatsController);
32  });
33});

Siz ushbu mocklarni testing konteyneridan odatdagi custom provayderlar kabi moduleRef.get(CatsService) bilan ham olishingiz mumkin.

Hint

Umumiy mock factory, masalan @golevelup/ts-jest paketidagi createMock ham bevosita uzatilishi mumkin.

Hint

REQUEST va INQUIRER provayderlarini auto-mock qilib bo'lmaydi, chunki ular kontekstda allaqachon aniqlangan. Biroq, ularni custom provayder sintaksisi yoki .overrideProvider metodi orqali override qilish mumkin.

End-to-end testlash

Unit testlashdan farqli o'laroq, end-to-end (e2e) testlash sinflar va modullar o'zaro ta'sirini ko'proq agregat darajada qamrab oladi -- bu production tizimida oxirgi foydalanuvchilar qanday o'zaro ta'sir qilishiga yaqin. Ilova kattalashgani sari har bir API endpointning end-to-end xatti-harakatini qo'lda testlash qiyinlashadi. Avtomatlashtirilgan end-to-end testlar tizimning umumiy xatti-harakati to'g'ri ekanini va loyiha talablariga javob berishini ta'minlaydi. E2e testlarni bajarish uchun biz hozirgina ko'rib chiqqan unit testing dagi konfiguratsiyaga o'xshashini ishlatamiz. Bundan tashqari, Nest HTTP so'rovlarini simulyatsiya qilish uchun Supertest kutubxonasidan foydalanishni osonlashtiradi.

TypeScript
cats.e2e-spec
1import * as request from 'supertest';
2import { Test } from '@nestjs/testing';
3import { CatsModule } from '../../src/cats/cats.module';
4import { CatsService } from '../../src/cats/cats.service';
5import { INestApplication } from '@nestjs/common';
6
7describe('Cats', () => {
8  let app: INestApplication;
9  let catsService = { findAll: () => ['test'] };
10
11  beforeAll(async () => {
12    const moduleRef = await Test.createTestingModule({
13      imports: [CatsModule],
14    })
15      .overrideProvider(CatsService)
16      .useValue(catsService)
17      .compile();
18
19    app = moduleRef.createNestApplication();
20    await app.init();
21  });
22
23  it(`/GET cats`, () => {
24    return request(app.getHttpServer())
25      .get('/cats')
26      .expect(200)
27      .expect({
28        data: catsService.findAll(),
29      });
30  });
31
32  afterAll(async () => {
33    await app.close();
34  });
35});
Hint

Agar HTTP adapter sifatida Fastify dan foydalansangiz, u biroz boshqacha konfiguratsiyani talab qiladi va o'zining ichki testing imkoniyatlariga ega:

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 */); }); }); afterAll(async () => { await app.close(); });

Ushbu misolda biz oldin ta'riflangan ayrim tushunchalarga tayanganmiz. Oldin ishlatgan compile() metodidan tashqari, endi createNestApplication() metodidan foydalanib to'liq Nest runtime muhitini instansiyalaymiz.

E'tibor berish kerak bo'lgan bir cheklov shuki, ilova compile() metodi yordamida kompilyatsiya qilinganda HttpAdapterHost#httpAdapter bu vaqtda undefined bo'ladi. Sababi, kompilyatsiya bosqichida HTTP adapter yoki server hali yaratilmagan bo'ladi. Agar test httpAdapter ga muhtoj bo'lsa, createNestApplication() metodidan foydalanib ilova instansiyasini yarating yoki loyihangizni bog'liqliklar grafini inicializatsiya qilishda bu qaramlikdan qochadigan qilib refaktor qiling.

Yaxshi, endi misolni tahlil qilamiz:

Biz ishlayotgan ilovaga havola olish uchun app o'zgaruvchisiga saqlab qo'yamiz, shunda undan HTTP so'rovlarni simulyatsiya qilishda foydalanamiz.

Supertest'dagi request() funksiyasi orqali HTTP testlarni simulyatsiya qilamiz. Bu HTTP so'rovlar ishlayotgan Nest ilovasiga yo'naltirilishi kerak, shuning uchun request() ga Nestning ostidagi HTTP listenerga havola uzatamiz (u o'z navbatida Express platformasi tomonidan taqdim etilishi mumkin). Shu sababli request(app.getHttpServer()) konstruktsiyasidan foydalanamiz. request() chaqiruvi bizga o'ralgan HTTP Serverni beradi; u endi Nest ilovaga ulangan va real HTTP so'rovni simulyatsiya qilish uchun metodlarni taqdim etadi. Masalan, request(...).get('/cats') chaqiruvi tarmoq orqali kelgan real HTTP so'rovga xuddi shunday bo'lgan Nest ilovasiga so'rovni boshlaydi.

Ushbu misolda biz CatsService ning muqobil (test-double) implementatsiyasini ham taqdim etamiz; u test qilib ko'rishimiz mumkin bo'lgan hard-coded qiymatni qaytaradi. Bunday muqobil implementatsiyani berish uchun overrideProvider() dan foydalaning. Xuddi shuningdek, Nest overrideModule(), overrideGuard(), overrideInterceptor(), overrideFilter() va overridePipe() metodlari bilan modullar, guardlar, interceptorlar, filterlar va pipe'larni override qilish imkonini beradi.

Har bir override metodi (overrideModule() dan tashqari) custom providers da ta'riflanganlarga mos 3 xil metodga ega obyektni qaytaradi:

  • useClass: instansiya taqdim etish uchun instansiyalanadigan sinfni berasiz (provider, guard va hokazo obyektni override qiladi).
  • useValue: obyektni override qiladigan instansiyani berasiz.
  • useFactory: obyektni override qiladigan instansiyani qaytaradigan funksiyani berasiz.

Boshqa tomondan, overrideModule() useModule() metodiga ega obyektni qaytaradi, u bilan original modulni override qiladigan modulni berishingiz mumkin, quyidagicha:

TypeScript
1const moduleRef = await Test.createTestingModule({
2  imports: [AppModule],
3})
4  .overrideModule(CatsModule)
5  .useModule(AlternateCatsModule)
6  .compile();

Har bir override metodi turi o'z navbatida TestingModule instansiyasini qaytaradi va shuning uchun u boshqa metodlar bilan fluent uslubda zanjirlanishi mumkin. Bunday zanjir oxirida compile() ni chaqirish kerak, shunda Nest modulni instansiyalaydi va inicializatsiya qiladi.

Shuningdek, ba'zida testlar ishlayotganida (masalan, CI serverda) maxsus logger taqdim etishni xohlashingiz mumkin. setLogger() metodidan foydalaning va testlar paytida loglash qanday bo'lishini LoggerService interfeysiga mos obyektni uzatib ko'rsating (standart holatda faqat "error" loglar konsolga chiqariladi).

Kompilyatsiya qilingan modul bir nechta foydali metodlarga ega, ular quyidagi jadvalda ta'riflangan:

createNestApplication() Berilgan modul asosida Nest ilovasini yaratadi va qaytaradi (INestApplication instansiyasi). E'tibor bering, ilovani init() metodi yordamida qo'lda inicializatsiya qilishingiz kerak.
createNestMicroservice() Berilgan modul asosida Nest mikroservisini yaratadi va qaytaradi (INestMicroservice instansiyasi).
get() Ilova kontekstida mavjud bo'lgan controller yoki provayderning (jumladan guardlar, filterlar va hokazo) statik instansiyasini oladi. module reference sinfidan meros olingan.
resolve() Ilova kontekstida mavjud bo'lgan controller yoki provayderning (jumladan guardlar, filterlar va hokazo) dinamik yaratilgan scoped instansiyasini (request yoki transient) oladi. module reference sinfidan meros olingan.
select() Modulning dependency graph'i bo'ylab navigatsiya qiladi; tanlangan moduldan aniq instansiyani olish uchun ishlatiladi (`get()` metodida strict mode (strict: true) bilan birga ishlatiladi).
Hint

E2e test fayllarini test katalogida saqlang. Test fayllari .e2e-spec suffiksiga ega bo'lishi kerak.

Global ro'yxatdan o'tkazilgan enhancerlarni override qilish

Agar sizda global ro'yxatdan o'tkazilgan guard (yoki pipe, interceptor, yoki filter) bo'lsa, bu enhancerni override qilish uchun yana bir nechta qadam bajarish kerak bo'ladi. Qisqacha eslatma: original ro'yxatdan o'tkazish quyidagicha:

TypeScript
1providers: [
2  {
3    provide: APP_GUARD,
4    useClass: JwtAuthGuard,
5  },
6],

Bu guardni APP_* token orqali "multi"-provider sifatida ro'yxatdan o'tkazadi. Bu joyda JwtAuthGuard ni almashtirish uchun ro'yxatdan o'tkazish shu slotda mavjud provayderdan foydalanishi kerak:

TypeScript
1providers: [
2  {
3    provide: APP_GUARD,
4    useExisting: JwtAuthGuard,
5    // ^^^^^^^^ notice the use of 'useExisting' instead of 'useClass'
6  },
7  JwtAuthGuard,
8],
Hint

useClass o'rniga useExisting ni qo'llang, shunda Nest token ortida instansiya yaratish o'rniga ro'yxatdan o'tgan provayderga havola qiladi.

Endi JwtAuthGuard Nest uchun oddiy provayder sifatida ko'rinadi va TestingModule yaratishda override qilinishi mumkin:

TypeScript
1const moduleRef = await Test.createTestingModule({
2  imports: [AppModule],
3})
4  .overrideProvider(JwtAuthGuard)
5  .useClass(MockAuthGuard)
6  .compile();

Endi barcha testlaringiz har bir so'rovda MockAuthGuard ni ishlatadi.

Request-scoped instansiyalarni testlash

Request-scoped provayderlar har bir kiruvchi so'rov uchun alohida yaratiladi. So'rov qayta ishlanishi tugagach, instansiya garbage-collected bo'ladi. Bu muammo tug'diradi, chunki test qilinayotgan so'rov uchun maxsus yaratilgan dependency injection sub-tree'ga kira olmaymiz.

Yuqoridagi bo'limlarga asoslanib, resolve() metodi dinamik instansiyalangan sinfni olish uchun ishlatilishi mumkinligini bilamiz. Shuningdek, bu yerda aytilganidek, DI container sub-tree'ning lifecycle'ini boshqarish uchun noyob context identifier uzatishimiz mumkinligini bilamiz. Buni test kontekstida qanday ishlatamiz?

Strategiya shundaki, oldindan context identifier yaratib, Nestni barcha kiruvchi so'rovlar uchun aynan shu IDdan sub-tree yaratishga majbur qilamiz. Shu tarzda test qilinayotgan so'rov uchun yaratilgan instansiyalarga kira olamiz.

Buni amalga oshirish uchun ContextIdFactory ga jest.spyOn() ni qo'llang:

TypeScript
1const contextId = ContextIdFactory.create();
2jest
3  .spyOn(ContextIdFactory, 'getByRequest')
4  .mockImplementation(() => contextId);

Endi contextId yordamida keyingi har qanday so'rov uchun bitta yaratilgan DI container sub-tree'ga kirishimiz mumkin.

TypeScript
1catsService = await moduleRef.resolve(CatsService, contextId);