IlmHamroh
JavaScript Full-stack/30-qism. NestJS49/65-dars24 daqiqa
Mundarija (29)

NestJS unit testlari: Test.createTestingModule, soxta provider va overrideProvider

Qisqacha: Nest'da unit test — bitta klassni haqiqiy DI orqali, lekin soxta bog'liqliklar bilan yaratib tekshirish. Test.createTestingModule({ providers: [...] }) kichik test modulini yasaydi, .compile() uni yig'adi, .get(Klass) tayyor nusxani beradi. Bog'liqlik o'rniga { provide: TOKEN, useValue: soxta } yoziladi; tayyor modulda esa overrideProvider(TOKEN).useValue(...) almashtiradi. Guard, pipe va interceptor — oddiy klass: ularni new bilan yaratib, soxta ExecutionContext berib sinaysiz.

Bu darsda

  • Test.createTestingModule → compile → get zanjirini va u Nest konteyneridan nimasi bilan farq qilishini tushuntirasiz.
  • Xizmatni soxta ombor bilan sinaysiz va vi.fn() chaqiruvlarini tekshirasiz.
  • useValue va overrideProvider ni qachon ishlatishni bilasiz, Proxy soxtasining then tuzog'ini tanib olasiz.
  • Guard, pipe va interceptor'ni HTTP serversiz, alohida sinaysiz.
  • Nest 12 da nega sukut test vositasi Vitest ekanini va Jest bilan o'lchov natijasini bilasiz.

Oldin bilishingiz kerak: Mock, stub, spy va fake, Vitest II: mock, spy, taymerlar va coverage, DI va IoC konteyner, Custom provider'lar, Guard'lar, Interceptor'lar va RxJS.

O'tgan darsda modular monolit haqida gaplashdik: modul chegaralari qat'iy bo'lsa, har bo'lakni alohida almashtirish mumkin. Testda ham xuddi shu g'oya ishlaydi — bitta klassni olib, atrofidagilarni soxtasi bilan almashtiramiz.

1. Nega bu kerak?

25–27-qismlarda vazifalar-api ning 130 ga yaqin testi bor edi. Ularning ko'pi HTTP integratsiya testi: haqiqiy so'rov yuboriladi, haqiqiy Express (endi Nest) uni qayta ishlaydi, javob tekshiriladi (Backend test strategiyasi). Bu to'g'ri tanlov — foydalanuvchi aynan shu shartnomani ko'radi.

Lekin ba'zi qoidalar HTTP orqali sinash uchun noqulay. Masalan, VazifalarService ichida qoida bor: vazifa false dan true ga o'tgandagina "vazifa bajarildi" hodisasi chiqadi. Ikkinchi marta true yuborilsa — hodisa chiqmaydi. Buni HTTP'dan tekshirish uchun webhook qabul qiladigan server kerak bo'ladi.

Yana bir misol — guard. JsonTanaGuard noto'g'ri Content-Type ga 415 beradi. U to'g'ri ishlashini ko'rish uchun butun ilovani ko'tarish shart emas: guard — oddiy klass, uning bitta metodi bor.

O'xshatish: avtomobil zavodida dvigatelni mashinaga o'rnatishdan oldin stendda sinashadi. Stend dvigatelga yoqilg'i, havo va elektr beradi — haqiqiy mashina bo'lmasa ham. Unit test — shu stend. Nest DI konteyneri esa stendni yasashni osonlashtiradi: bog'liqliklarni klass o'zi yaratmaydi, tashqaridan oladi (DI va IoC konteyner). Demak, testda ularni soxtasi bilan almashtirish mumkin.

2. Test moduli: createTestingModule → compile → get

2.1 Shablondagi birinchi test

nest new yaratgan loyihada bitta unit test bor edi (Nest CLI va loyiha tuzilmasi). Uni endi to'liq o'qiymiz:

ts
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  it('should return "Hello World!"', () => {
    expect(appController.getHello()).toBe('Hello World!');
  });
});

Uch qadam bor:

Qadam Nima qiladi Ilovadagi o'xshashi
Test.createTestingModule({...}) test moduli tavsifi — @Module dagi maydonlar @Module({...})
.compile() DI konteynerni yig'adi, nusxalarni yaratadi NestFactory.create
.get(Klass) tayyor nusxani beradi konteyner ichidagi nusxa

@nestjs/testing paketi @nestjs/core bilan bir versiyada — kanonda 12.2.0. Test moduli HTTP server ochmaydi, port olmaydi, middleware ulanmaydi. U faqat DI qismini quradi. Shuning uchun tez ishlaydi.

Maslahat: compile() Promise qaytaradi — await ni unutmang. Unutsangiz, .get metodi Promise'da yo'q degan xato olasiz.

2.2 Nimani sinaymiz, nimani almashtiramiz

Unit testda bitta klass haqiqiy bo'ladi — siz sinayotgan klass. Uning bog'liqliklari — soxta (mock, Mock, stub, spy va fake). Bizning asosiy misol — VazifalarService. Unga uchta narsa kerak: ombor (VAZIFA_OMBORI token bilan), log (LOG) va hodisalar (EventEmitter2):

flowchart LR
  T["Test.createTestingModule"] --> X["VazifalarService (haqiqiy)"]
  O["VAZIFA_OMBORI: soxta ombor"] --> X
  L["LOG: jim log"] --> X
  E["EventEmitter2: haqiqiy, bo'sh"] --> X
  X --> N["xizmat.yarat() natijasi"]
  O -. "vi.fn() chaqiruvlari" .-> K["expect(...)"]
  E -. "hodisa tinglovchisi" .-> K

Diagrammaga qarang: o'rtadagi xizmat — haqiqiy kod. Chapdagi uchta bog'liqlik — test bergan qiymatlar. Pastdagi punktir chiziqlar — test kuzatadigan joylar: ombor qanday chaqirildi, qaysi hodisa chiqdi.

EventEmitter2 ni soxtalashtirmadik — haqiqiy, lekin yangi va bo'sh nusxa berdik. Bu ham to'g'ri yo'l: obyekt sodda va tez bo'lsa, haqiqiysi soxtasidan yaxshiroq. Ombor esa bazaga boradi — uni albatta almashtiramiz.

Tekshirib ko'ring: Test moduli NestFactory.create dan nimasi bilan farq qiladi?

Javob

Test moduli faqat DI konteynerni quradi: provider va controller nusxalari. HTTP server, middleware, global pipe va filtrlar yo'q, port ochilmaydi. Shuning uchun u tez, va unda sinalayotgan klass faqat o'z bog'liqliklariga tayanadi. HTTP qatlami ham kerak bo'lsa — keyingi darsdagi e2e test.

3. JS modelida test moduli

Test moduli ichida nima bo'layotganini kichik modelda takrorlaymiz. Bu blok brauzerda ishlaydi, dekorator va paket yo'q. Klass o'ziga kerak tokenlarni static kerak ro'yxatida aytadi — Nest buni @Inject va emitDecoratorMetadata dan oladi (Reflector va metadata):

js
const OMBOR = "OMBOR";

class VazifalarXizmati {
  static kerak = [OMBOR];
  constructor(ombor) {
    this.ombor = ombor;
  }
  async ol(id) {
    const v = await this.ombor.ol(id);
    if (!v) throw new Error(`Vazifa ${id} topilmadi (404)`);
    return v;
  }
}

// Test moduli modeli: klass yoki { provide, useValue }
function testModuli(providers) {
  const kalit = (p) => p.provide ?? p;
  const royxat = new Map(providers.map((p) => [kalit(p), p]));
  const nusxalar = new Map();
  function get(token) {
    if (nusxalar.has(token)) return nusxalar.get(token);
    const p = royxat.get(token);
    if ("useValue" in p) return p.useValue;
    const args = p.kerak.map((dep, i) => {
      if (!royxat.has(dep)) {
        throw new Error(`${p.name} ning ${i}-argumenti (${dep}) ` +
          "test modulida yo'q");
      }
      return get(dep);
    });
    const nusxa = new p(...args);
    nusxalar.set(token, nusxa);
    return nusxa;
  }
  return {
    overrideProvider(token) {
      return {
        useValue(qiymat) {
          royxat.set(token, { provide: token, useValue: qiymat });
        },
      };
    },
    compile() {
      for (const token of royxat.keys()) get(token);
      return { get };
    },
  };
}

// 1) bog'liqlik unutildi
try {
  testModuli([VazifalarXizmati]).compile();
} catch (e) {
  console.log(e.message);
}

// 2) haqiqiy xizmat + soxta ombor
const chaqiruvlar = [];
const soxtaOmbor = {
  ol: async (id) => {
    chaqiruvlar.push(id);
    return id === 1 ? { id: 1, matn: "Non olish" } : undefined;
  },
};
const m = testModuli([
  VazifalarXizmati,
  { provide: OMBOR, useValue: soxtaOmbor },
]).compile();
const xizmat = m.get(VazifalarXizmati);
console.log(await xizmat.ol(1));
try {
  await xizmat.ol(5);
} catch (e) {
  console.log(e.message);
}
console.log("ombor chaqiruvlari:", chaqiruvlar);

// 3) overrideProvider — compile'dan oldin almashtiramiz
const t = testModuli([
  VazifalarXizmati,
  { provide: OMBOR, useValue: null },
]);
t.overrideProvider(OMBOR).useValue({ ol: async () => ({ id: 9 }) });
console.log(await t.compile().get(VazifalarXizmati).ol(9));

Konsolda:

text
VazifalarXizmati ning 0-argumenti (OMBOR) test modulida yo'q
{ id: 1, matn: 'Non olish' }
Vazifa 5 topilmadi (404)
ombor chaqiruvlari: [ 1, 5 ]
{ id: 9 }

Uch holatga qarang:

  1. Ombor ro'yxatda yo'q — compile() darhol yiqildi. Nest ham shunday qiladi, birinchi testgacha.
  2. Soxta ombor berildi — xizmatning haqiqiy kodi ishladi: 1 topildi, 5 uchun 404 xatosi. chaqiruvlar massivi — qo'lda yozilgan "josus" (spy): ombor qaysi argument bilan chaqirilganini eslab qoldi.
  3. overrideProvider — ro'yxatdagi yozuvni compile() dan oldin almashtirdi.

Model soddalashtirilgan: unda scope, async fabrika va modul chegarasi yo'q. Lekin asosiy g'oya aynan shu — konteyner tokenga qarab qiymat beradi, test esa qiymatni o'zi tanlaydi.

4. VazifalarService ni soxta ombor bilan sinash

4.1 Soxta ombor: vi.fn() va Proxy

Vitest'dagi vi.fn() ni 22-qismda o'rgangansiz (Vitest II): u chaqiruvlarni eslab qoladigan soxta funksiya. mockResolvedValue(x) — har chaqiruvda Promise.resolve(x) qaytaradi, mockResolvedValueOnce(x) — faqat navbatdagi bitta chaqiruvda.

VazifaOmbori interfeysida o'nga yaqin metod bor. Har testda hammasini yozish og'ir. Kanon kichik yordamchi yozdi — Proxy asosidagi soxta ombor. Proxy — obyekt o'rniga turib, har xususiyat so'rovini ushlaydigan o'rovchi. Test bergan metodni qaytaradi, qolgan har qanday metodga "kutilmagan chaqiruv" xatosini tashlaydi:

ts
type Soxtalar = Partial<
  Record<keyof VazifaOmbori, ReturnType<typeof vi.fn>>
>;

/** VazifaOmbori soxtasi: faqat test kerak qilgan metodlar
 * (qolgani chaqirilsa — xato) */
function soxtaOmbor(ust: Soxtalar): VazifaOmbori {
  // "then" — Nest useValue qiymati Promise emasligini tekshiradi
  // (thenable) — undefined qaytsin
  return new Proxy({} as VazifaOmbori, {
    get: (_t, k: string) => {
      if (k === "then") return undefined;
      return (
        ust[k as keyof VazifaOmbori] ??
        (() => {
          throw new Error(`kutilmagan chaqiruv: ombor.${k}`);
        })
      );
    },
  });
}

Nega "kutilmagan chaqiruv" xatosi foydali? Xizmat kodini kimdir o'zgartirib, ombordan yangi metod chaqira boshlasa, test darhol aytadi. Jim undefined qaytarganda esa test noto'g'ri sabab bilan o'tib ketishi mumkin edi.

4.2 Topilma: then tuzog'i

Izohdagi "then" qatori — kanon ko'chish paytida topgan haqiqiy muammo. Shu qatorsiz Proxy'ni sinab ko'rdik (biz ishga tushirdik, NestJS 12.2.0, Vitest 5.0.5):

text
XATO: kutilmagan chaqiruv: ombor.then

Test hali bitta ham metod chaqirmagan, lekin compile() yiqildi. Sabab: Nest useValue ga berilgan qiymatni tekshiradi — u Promise emasmi? JavaScript'da "Promise'ga o'xshash" (thenable) obyektni bilish usuli bitta: qiymat.then funksiyami. Proxy esa har qanday nom uchun funksiya qaytaradi — then uchun ham. Nest uni Promise deb chaqirdi, funksiya xato tashladi.

Saboq kengroq: Proxy soxtasi then ga undefined qaytarsin. Bu faqat Nest'da emas — await soxta yozilgan har joyda shu tuzoq bor.

4.3 Test moduli yordamchisi

Har testda bir xil to'rt provider yozilmasligi uchun kanon yordamchi funksiya qildi:

ts
async function xizmatModuli(ombor: VazifaOmbori) {
  const m = await Test.createTestingModule({
    providers: [
      VazifalarService,
      { provide: VAZIFA_OMBORI, useValue: ombor },
      { provide: LOG, useValue: logYarat("silent") },
      { provide: EventEmitter2, useValue: new EventEmitter2() },
    ],
  }).compile();
  return {
    xizmat: m.get(VazifalarService),
    hodisalar: m.get(EventEmitter2),
  };
}

VazifalarService — haqiqiy klass, ro'yxatda o'zi turibdi. Qolganlari useValue bilan berilgan (Custom provider'lar). logYarat("silent") — haqiqiy pino logger, lekin hech narsa chiqarmaydi: test natijasi log bilan to'lib ketmaydi.

4.4 Uchta xizmat testi

ts
describe("VazifalarService (mock ombor, DI)", () => {
  test("yarat: ombor chaqiriladi, 'vazifa.yaratildi' chiqadi",
    async () => {
      const yarat = vi.fn().mockResolvedValue(v({ id: 7 }));
      const soni = vi.fn().mockResolvedValue(0);
      const { xizmat, hodisalar } = await xizmatModuli(
        soxtaOmbor({ soni, yarat }),
      );
      const tinglovchi = vi.fn();
      hodisalar.on(HODISA.yaratildi, tinglovchi);
      await expect(xizmat.yarat({ matn: "Non" }))
        .resolves.toMatchObject({ id: 7 });
      expect(yarat).toHaveBeenCalledWith({ matn: "Non" });
      expect(tinglovchi).toHaveBeenCalledWith(
        expect.objectContaining({ id: 7 }),
      );
    });

  test("ol: ombor undefined → TopilmadiXatosi (404)", async () => {
    const ol = vi.fn().mockResolvedValue(undefined);
    const { xizmat } = await xizmatModuli(soxtaOmbor({ ol }));
    await expect(xizmat.ol(5))
      .rejects.toBeInstanceOf(TopilmadiXatosi);
  });

  test("yangila: faqat false → true da 'vazifa.bajarildi'",
    async () => {
      const ol = vi.fn()
        .mockResolvedValueOnce(v({ bajarildi: false }))
        .mockResolvedValueOnce(v({ bajarildi: true }));
      const yangila = vi.fn().mockResolvedValue({
        tur: "ok",
        qiymat: v({ bajarildi: true, versiya: 2 }),
      });
      const { xizmat, hodisalar } = await xizmatModuli(
        soxtaOmbor({ ol, yangila }),
      );
      const bajarildi = vi.fn();
      hodisalar.on(HODISA.bajarildi, bajarildi);
      await xizmat.yangila(1, { bajarildi: true });
      await xizmat.yangila(1, { bajarildi: true });
      expect(bajarildi).toHaveBeenCalledTimes(1);
    });
});

Har testni qisqacha o'qiymiz:

  • yarat — xizmat avval soni() ni so'raydi (ro'yxat 1000 ta bilan cheklangan), keyin yarat() ni. Soxta soni 0 qaytaradi. Test uch narsani tekshiradi: natija, ombor qanday argument bilan chaqirilgani va hodisa chiqqani.
  • ol — ombor undefined qaytarsa, xizmat 404 xatosini (TopilmadiXatosi) tashlashi kerak. HTTP yo'q — faqat xato klassi tekshiriladi. 404 statusga aylanishi — filtrning ishi (Exception filter'lar), bu boshqa test.
  • yangila — eng qiziq qoida. ol ikki marta chaqiriladi: birinchisida eski holat false, ikkinchisida true. mockResolvedValueOnce zanjiri shu ketma-ketlikni beradi. Ikki yangila dan keyin bajarildi hodisasi faqat bir marta chiqqan bo'lishi kerak.

v() — kichik yordamchi: to'liq Vazifa obyektini yasaydi, test faqat o'zgargan maydonni beradi (v({ id: 7 })). Bu — 27-qismdagi factory g'oyasining eng sodda shakli.

Tekshirib ko'ring: yangila testida mockResolvedValueOnce o'rniga mockResolvedValue(v({ bajarildi: false })) yozilsa, test nima ko'rsatadi?

Javob

Ikkala ol chaqiruvi ham false qaytaradi. Xizmat ikkala safar ham "false → true o'tish bo'ldi" deb o'ylaydi va hodisani ikki marta chiqaradi. toHaveBeenCalledTimes(1) yiqiladi. Soxtaning javoblari ketma-ketligi haqiqiy hayotni takrorlashi kerak: birinchi yangila dan keyin bazada true turadi.

5. Bog'liqlik unutilsa — haqiqiy xabar

Test modulida LOG ni yozishni unutdik (biz ishga tushirdik):

text
Nest can't resolve dependencies of the VazifalarService (Symbol(VAZIFA_OMBORI), ?, EventEmitter). Please make sure that the argument Symbol(LOG) at index [1] is available in the RootTestModule module.

Potential solutions:
- Is RootTestModule a valid NestJS module?
- If Symbol(LOG) is a provider, is it part of the current RootTestModule?
- If Symbol(LOG) is exported from a separate @Module, is that module imported within RootTestModule?
  @Module({
    imports: [ /* the Module containing Symbol(LOG) */ ]
  })

For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors

Tarjimasi: "Nest VazifalarService ning bog'liqliklarini topa olmadi. RootTestModule modulida 1-indeksdagi Symbol(LOG) argumenti mavjudligiga ishonch hosil qiling."

Bu xabarni Modullar darsida o'qishni o'rgangansiz. Ikki yangi narsa bor:

  • RootTestModule — createTestingModule yaratgan yashirin modul nomi. Xabarda shu nomni ko'rsangiz, muammo test sozlamasida, ilova kodida emas.
  • Symbol(LOG) — token klass emas, Symbol bo'lgani uchun shunday ko'rinadi. (Symbol(VAZIFA_OMBORI), ?, EventEmitter) — uchta argument, ikkinchisi topilmadi.

6. overrideProvider — tayyor ro'yxatda almashtirish

useValue ni ro'yxatga o'zingiz yozasiz. Lekin ba'zan ro'yxat sizniki emas — masalan, butun VazifalarModule ni import qilasiz va uning ichidagi bitta provider'ni almashtirmoqchisiz. Bunga overrideProvider bor:

ts
test("overrideProvider: XABARCHI almashtirildi, hodisa → xabarchi",
  async () => {
    const xabarchi = vi.fn();
    const m = await Test.createTestingModule({
      providers: [
        WebhookTinglovchi,
        { provide: XABARCHI, useValue: null },
      ],
    })
      .overrideProvider(XABARCHI).useValue(xabarchi)
      .compile();
    m.get(WebhookTinglovchi).bajarildi(v({ bajarildi: true }));
    expect(xabarchi).toHaveBeenCalledWith(
      "vazifa.bajarildi",
      expect.objectContaining({ id: 1 }),
    );
  });

WebhookTinglovchi — Hodisalar darsidagi tinglovchi: "vazifa bajarildi" hodisasini olib, webhook xabarchisiga beradi. Ro'yxatda XABARCHI null (ilovada webhook sozlanmagan holat). overrideProvider(XABARCHI).useValue(xabarchi) uni compile() dan oldin vi.fn() bilan almashtirdi.

overrideProvider dan keyin to'rt usul bor: useValue, useClass, useFactory va useExisting. Ma'nosi providers dagi bilan bir xil. Xuddi shunday overrideGuard, overridePipe, overrideInterceptor, overrideFilter ham bor — global yoki @UseGuards bilan ulangan enhancer'ni almashtirish uchun. Ularni keyingi darsda e2e testda ishlatamiz.

Usul Qachon
providers da useValue ro'yxatni o'zingiz yozasiz (eng ko'p uchraydi)
overrideProvider modulni butunlay import qildingiz, ichidan bittasini almashtirasiz
overrideGuard va boshqalar enhancer'ni o'chirish yoki soxtalash (e2e)

Diqqat: overrideProvider faqat compile() dan oldin ishlaydi. Tayyor modulda provider allaqachon yaratilgan — keyin almashtirib bo'lmaydi.

7. Guard, pipe va interceptor — alohida

Ilovada bu uchtasi Request lifecycle zanjirining bo'g'inlari: so'rov middleware → guard → interceptor → pipe → handler → interceptor → xato filtri tartibida o'tadi. Unit testda zanjir yo'q — har bo'g'inni yolg'iz chaqiramiz. Shuning uchun Nest normalda o'zi beradigan narsani test qo'lda beradi:

Bo'g'in Ilovada Nest beradi Testda biz beramiz
guard ExecutionContext, metama'lumot soxta kontekst, Reflect.defineMetadata
pipe qiymat va ArgumentMetadata ikkalasini qo'lda
interceptor kontekst va keyingi bo'g'in (CallHandler) soxta kontekst va of(...)

Zanjirning o'zi — tartib, global ulanish, filtr xatoni javobga aylantirishi — keyingi darsdagi e2e testning ishi.

7.1 Soxta ExecutionContext

Guard va interceptor ExecutionContext oladi (Guard'lar). Haqiqiy kontekstni Nest so'rov paytida yasaydi. Testda bizga uning faqat bir nechta metodi kerak — shularni oddiy obyekt bilan beramiz:

ts
/** HTTP'siz ExecutionContext soxtasi (guard/interceptor uchun) */
function kontekst(
  req: object,
  handler: () => unknown,
  res: object = {},
): ExecutionContext {
  return {
    getHandler: () => handler,
    getClass: () => Object,
    switchToHttp: () => ({
      getRequest: () => req,
      getResponse: () => res,
      getNext: () => undefined,
    }),
  } as unknown as ExecutionContext;
}

as unknown as ExecutionContext — TypeScript'ga "bu obyekt to'liq emas, bilib turibman" degani. Interfeysning qolgan metodlari (getArgs, switchToRpc …) yo'q: ularni chaqirsa, test darhol yiqiladi va qaysi metod kerakligini ko'rsatadi.

7.2 Guard testi

ts
test("JsonTanaGuard: belgisiz — o'tadi; belgili + text/plain — 415",
  () => {
    const guard = new JsonTanaGuard(new Reflector());
    const belgisiz = () => 1;
    const belgili = () => 2;
    // @JsonTana() xuddi shu metama'lumotni yozadi
    Reflect.defineMetadata("vazifalar:json-tana", true, belgili);
    const req = { is: () => false };
    expect(guard.canActivate(kontekst(req, belgisiz))).toBe(true);
    expect(() => guard.canActivate(kontekst(req, belgili)))
      .toThrow(expect.objectContaining({ holat: 415 }));
  });

Bu yerda test moduli umuman yo'q: new JsonTanaGuard(new Reflector()). Guard'ning yagona bog'liqligi — Reflector, uni o'zimiz yaratdik. Bog'liqlik kam bo'lsa, new — eng sodda yo'l.

Reflect.defineMetadata — @JsonTana() dekoratori qiladigan ishni qo'lda bajaradi: handler funksiyasiga belgi qo'yadi (Custom dekoratorlar). req.is — Express'ning "so'rov shu turdami" metodi; soxtasi doim false — ya'ni so'rov JSON emas.

7.3 Pipe testi

ts
test("SxemaPipe: body → 422, param → 400, sxemasiz — o'zgarishsiz",
  async () => {
    const pipe = new SxemaPipe();
    const sxema = z.strictObject({
      matn: z.string().min(1, { error: "bo'sh" }),
    });
    const body = { type: "body", schema: sxema } as ArgumentMetadata;
    await expect(pipe.transform({ matn: "" }, body)).rejects
      .toSatisfy((x) => x instanceof ValidatsiyaXatosi &&
        x.xatolar?.[0]?.joy === "body.matn");
    const param = {
      type: "param",
      data: "id",
      schema: z.string().regex(/^\d+$/),
    } as ArgumentMetadata;
    await expect(pipe.transform("abc", param)).rejects
      .toSatisfy((x) => x instanceof ParametrXatosi &&
        x.xatolar?.[0]?.joy === "params.id");
    const sxemasiz = { type: "body" } as ArgumentMetadata;
    await expect(pipe.transform({ x: 1 }, sxemasiz))
      .resolves.toEqual({ x: 1 });
  });

Pipe — transform(qiymat, metadata) metodi bor klass (Pipe'lar). ArgumentMetadata ni Nest dekoratorlardan yig'adi: @Body({ schema }) → { type: "body", schema }. Testda uni qo'lda yozamiz. Uch holat — kanonning uch qoidasi: tana xatosi 422, yo'l parametri xatosi 400, sxemasiz parametr tegilmaydi (Zod va Standard Schema).

7.4 Interceptor testi

ts
test("VazifaSarlavhalariInterceptor: ETag va Last-Modified",
  async () => {
    const sarlavhalar: Record<string, string> = {};
    const res = {
      set: (k: string, q: string) => {
        sarlavhalar[k] = q;
      },
    };
    const keyingi: CallHandler<Vazifa> = {
      handle: () => of(v({ versiya: 4 })),
    };
    const ic = new VazifaSarlavhalariInterceptor();
    await lastValueFrom(
      ic.intercept(kontekst({}, () => 0, res), keyingi),
    );
    expect(sarlavhalar).toEqual({
      ETag: '"4"',
      "Last-Modified": "Wed, 28 Oct 2026 00:00:00 GMT",
    });
  });

Interceptor handler'ni CallHandler orqali chaqiradi va Observable qaytaradi (Interceptor'lar va RxJS). Testda handler — of(...): bitta qiymat chiqarib tugaydigan Observable. lastValueFrom Observable'ni Promise'ga aylantiradi — await bilan kutish uchun. res.set soxtasi qo'yilgan sarlavhalarni obyektga yig'adi.

Tekshirib ko'ring: Nega guard testida Test.createTestingModule ishlatilmadi, xizmat testida esa ishlatildi?

Javob

Guard'ning bitta oddiy bog'liqligi bor (Reflector) — new bilan yaratish qisqa va aniq. VazifalarService da uchta bog'liqlik, ulardan biri token (@Inject(VAZIFA_OMBORI)) orqali keladi. Test moduli tokenlarni Nest'dagidek bog'laydi va dekoratorlar to'g'ri yozilganini ham tekshiradi. Qoida: bog'liqlik ko'p yoki token orqali bo'lsa — test moduli; bitta-ikkita oddiy klass bo'lsa — new.

8. Natija: biz ishga tushirdik

Formatlangan test/birlik.test.ts ni loyiha nusxasida yurgizdik (NestJS 12.2.0, Vitest 5.0.5, Node 24.21.0; boshlanish vaqti olib tashlandi):

bash
npx vitest run test/birlik.test.ts --reporter=verbose
text
 ✓ test/birlik.test.ts > VazifalarService (mock ombor, DI) > yarat: ombor chaqiriladi, 'vazifa.yaratildi' chiqadi 196ms
 ✓ test/birlik.test.ts > VazifalarService (mock ombor, DI) > ol: ombor undefined → TopilmadiXatosi (404) 4ms
 ✓ test/birlik.test.ts > VazifalarService (mock ombor, DI) > yangila: faqat false → true da 'vazifa.bajarildi' 6ms
 ✓ test/birlik.test.ts > overrideProvider: XABARCHI almashtirildi, hodisa → xabarchi 3ms
 ✓ test/birlik.test.ts > JsonTanaGuard: belgisiz — o'tadi; belgili + text/plain — 415 1ms
 ✓ test/birlik.test.ts > SxemaPipe: body → 422, param → 400, sxemasiz — o'zgarishsiz 8ms
 ✓ test/birlik.test.ts > VazifaSarlavhalariInterceptor: ETag va Last-Modified 2ms

 Test Files  1 passed (1)
      Tests  7 passed (7)

Vaqtlarga qarang: birinchi test 196 ms, qolganlari 1–8 ms. Birinchi test Nest va Zod modullarini yuklash narxini to'laydi. Shu faylni birinchi marta (sovuq keshda) yurgizganimizda birinchi test 7 438 ms oldi — mashina band edi. Unit testlar tez, lekin import qimmat: fayl qancha ko'p paket yuklasa, shuncha sekin boshlanadi.

9. Vitest yoki Jest — Nest 12 da

Nest 11 gacha nest new loyihasiga Jest qo'yardi. Nest 12 shabloni ESM va Vitest bilan keladi (Nest CLI va loyiha tuzilmasi). Jest hali ham ishlaydi, eski Nest loyihalarining ko'pi Jest'da (Jest va eski test muhitlari).

Kanon ikkalasini bitta nest new demo loyihasida o'lchadi: bitta unit va bitta e2e test, uch aylana. Jest — 30.5.2 va @swc/jest 0.2.39, ESM rejimida (--experimental-vm-modules). Nima uchun bu muhim: Nest 12 ning o'z paketlari endi faqat ESM. Rasmiy ko'chish qo'llanmasi (migration guide, 2026-11 holatiga) aynan shuni ogohlantiradi — Jest bu paketlarni faqat Node 24.9 va yangisida yuklay oladi; eskiroq Node'da "testlarni Vitest'ga ko'chiring" deydi. Kanon Node 24.21 da ishladi. Bitta mashina, mashina band — mutlaq vaqt emas, nisbat muhim:

Kichik demo: ikki test vaqti (Jest = 100 %)
  • Jest (unit + e2e birga)mediana 4 772 ms100 %
  • Vitest (faqat unit)mediana 7 081 ms148 %
  • Vitest (unit + e2e, ikki yugurish)mediana 14 906 ms312 %

Manba: Kanon o'lchovi (IlmHamroh, 2026-10): nest new demo, 3 aylana, bitta mashina, bir vaqtda

Kichik loyihada Jest tezroq chiqdi. Sabab — Vitest har ishga tushishda Vite'ni ko'taradi, bu boshlang'ich narx. Shablonda unit va e2e uchun ikkita alohida config bor — demak ikki marta to'lanadi. Katta to'plamda bu narx ko'p testga bo'linadi. Kanonning o'z to'plamida (25 fayl, 131 test) Vitest 7–15 s oldi. Jest esa kanon to'plamida sinalmagan: 25 faylning importlarini qayta yozish kerak edi.

Kanon nega baribir Vitest'da qoldi:

Mezon Vitest Jest
Nest 12 sukuti ha yo'q (alohida sozlash)
ESM tabiiy --experimental-vm-modules bilan
API vi.fn, vi.mock jest.fn, jest.mock
Kichik demo tezligi sekinroq tezroq

Bitta muhim tafsilot: Vitest kodni Vite (oxc) bilan o'giradi, build esa tsc bilan. Ular dekoratorlarni har xil o'giradi. Kanonda shunday holat topilgan: aylanma import Vitest'da o'tadi, build'dan keyin esa ilova yiqiladi (Circular dependency). Shuning uchun loyihaning check skriptiga build ham qo'shiladi — buni standalone ilova darsida qilamiz.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Nest can't resolve dependencies … in the RootTestModule test modulida bog'liqlik yo'q providers ga { provide, useValue }
kutilmagan chaqiruv: ombor.then (yoki boshqa then xatosi) Proxy soxtasi then ga funksiya qaytardi then uchun undefined
.get is not a function compile() oldida await yo'q await Test.createTestingModule(...).compile()
overrideProvider ta'sir qilmadi compile() dan keyin chaqirildi almashtirish — compile() dan oldin
Test o'tdi, lekin ilovada xato soxta haqiqiy xulqni takrorlamaydi shartnoma testi (Backend test strategiyasi) va e2e

Oxirgi qator eng xavflisi. Soxta ombor siz yozgan narsani qaytaradi — haqiqiy Postgres esa boshqacha javob berishi mumkin. 26–27-qismlardagi shartnoma testlari shuning uchun bor: hamma omborlar bir xil test to'plamidan o'tadi. Unit test ular o'rnini bosmaydi, faqat qo'shimcha.

11. Mashqlar

Mashqlar kurs/mashqlar/30/49-unit-testlar/ da.

1-mashq (oson): zanjirni to'ldiring

  • Test moduli: await Test.createTestingModule({ providers }).().
  • Tayyor nusxani olish: m.(VazifalarService).
  • Token o'rniga tayyor qiymat: { provide: LOG, : logYarat("silent") }.
  • Import qilingan moduldagi provider'ni almashtirish: .(XABARCHI).useValue(f).
  • Bog'liqlik yo'q bo'lsa, xabardagi modul nomi: .
Yechim
  • compile — DI konteynerni yig'adi va Promise qaytaradi.
  • get — konteynerdagi tayyor nusxa.
  • useValue — klass yaratilmaydi, berilgan qiymat o'zi beriladi.
  • overrideProvider — compile() dan oldin chaqiriladi.
  • RootTestModule — createTestingModule yaratgan yashirin modul.

2-mashq (o'rta): soxta funksiya yozing

vi.fn() ning kichik modelini yozing: soxtaFn() funksiya qaytarsin. U chaqirilganda argumentlarni f.chaqiruvlar massiviga qo'shsin. f.qaytarsin(q) — keyingi hamma chaqiruvlar q ni qaytarsin; f.birMarta(q) — faqat navbatdagi bitta chaqiruv. Navbat bo'sh bo'lsa — qaytarsin qiymati. Ishora: bir martalik qiymatlar uchun massiv va shift() (massiv metodlari).

Yechim
js
function soxtaFn() {
  let doimiy;
  const navbat = [];
  const f = (...args) => {
    f.chaqiruvlar.push(args);
    return navbat.length > 0 ? navbat.shift() : doimiy;
  };
  f.chaqiruvlar = [];
  f.qaytarsin = (q) => {
    doimiy = q;
    return f;
  };
  f.birMarta = (q) => {
    navbat.push(q);
    return f;
  };
  return f;
}

const ol = soxtaFn()
  .birMarta({ bajarildi: false })
  .birMarta({ bajarildi: true })
  .qaytarsin(undefined);

console.log(ol(1));
console.log(ol(1));
console.log(ol(2));
console.log("chaqiruvlar:", ol.chaqiruvlar.length,
  JSON.stringify(ol.chaqiruvlar));

Konsolda:

text
{ bajarildi: false }
{ bajarildi: true }
undefined
chaqiruvlar: 3 [[1],[1],[2]]

Bu vi.fn().mockReturnValueOnce(a).mockReturnValueOnce(b) ning ishlash tartibi: avval bir martalik navbat, keyin doimiy qiymat. return f — zanjir yozish uchun (.birMarta(...).qaytarsin(...)). Haqiqiy vi.fn yana ko'p narsa eslaydi: this, natija, tashlangan xato.

3-mashq (qiyin): guard'ni soxta kontekst bilan sinash

Paketsiz model. rolGuard(kerakli) funksiyasi { canActivate(k) } obyektini qaytarsin: k.switchToHttp().getRequest().rol kerakli ga teng bo'lsa true, rol umuman yo'q bo'lsa — "401" xabarli Error, boshqa rol bo'lsa — "403" xabarli Error tashlasin. kontekst(req) soxta kontekst yasovchisini yozing va uchta holatni tekshirib, natijani chiqaring.

Yechim
js
function rolGuard(kerakli) {
  return {
    canActivate(k) {
      const { rol } = k.switchToHttp().getRequest();
      if (rol === undefined) throw new Error("401");
      if (rol !== kerakli) throw new Error("403");
      return true;
    },
  };
}

function kontekst(req) {
  return { switchToHttp: () => ({ getRequest: () => req }) };
}

const guard = rolGuard("admin");
for (const req of [{ rol: "admin" }, {}, { rol: "aziz" }]) {
  try {
    console.log(JSON.stringify(req), "→",
      guard.canActivate(kontekst(req)));
  } catch (e) {
    console.log(JSON.stringify(req), "→ xato", e.message);
  }
}

Konsolda:

text
{"rol":"admin"} → true
{} → xato 401
{"rol":"aziz"} → xato 403

Soxta kontekstda faqat guard ishlatadigan metod bor — switchToHttp().getRequest(). Haqiqiy loyihada xuddi shu test Vitest'da expect(() => ...).toThrow("403") bilan yoziladi. Guard 401 va 403 ni farqlashi — Global auth guard darsidagi qoida.

12. Loyiha qadami: test/birlik.test.ts

Bu va keyingi dars bitta qadam: vazifalar-api ga Nest unit testlari qo'shiladi, mavjud HTTP testlari e2e vazifasini bajaradi. Commit — NestJS e2e testlari darsida.

Yangi fayl test/birlik.test.ts. Uning hamma qismlari yuqorida — «Xizmatni soxta ombor bilan sinash», «overrideProvider» va «Guard, pipe va interceptor — alohida» bo'limlarida. Fayl boshidagi importlar va v() yordamchisi:

ts
// birlik.test.ts — Nest unit testlari: Test.createTestingModule,
// useValue mock'lar (vi.fn), overrideProvider, guard/pipe/interceptor
// alohida. 25–27 testlari asosan HTTP integratsiya (testing trophy);
// bu yerda — DI orqali bitta klassni alohida tekshirish naqshlari
import { describe, expect, test, vi } from "vitest";
import { Test } from "@nestjs/testing";
import { EventEmitter2 } from "@nestjs/event-emitter";
import type {
  ArgumentMetadata,
  CallHandler,
  ExecutionContext,
} from "@nestjs/common";
import { of, lastValueFrom } from "rxjs";
import { z } from "zod";
import {
  VazifalarService,
  HODISA,
} from "../src/vazifalar/vazifalar.service.ts";
import {
  WebhookTinglovchi,
} from "../src/vazifalar/vazifalar.tinglovchi.ts";
import { LOG, VAZIFA_OMBORI, XABARCHI } from "../src/tokenlar.ts";
import { logYarat } from "../src/umumiy/log.ts";
import { JsonTanaGuard } from "../src/umumiy/json-tana.guard.ts";
import { SxemaPipe } from "../src/umumiy/sxema.pipe.ts";
import {
  VazifaSarlavhalariInterceptor,
} from "../src/umumiy/vazifa-sarlavhalari.interceptor.ts";
import {
  ParametrXatosi,
  TopilmadiXatosi,
  ValidatsiyaXatosi,
} from "../src/umumiy/xatolar.ts";
import { Reflector } from "@nestjs/core";
import type { Vazifa } from "../src/vazifalar/vazifa.ts";
import type { VazifaOmbori } from "../src/vazifalar/ombor.ts";

const v = (o: Partial<Vazifa> = {}): Vazifa => ({
  id: 1,
  matn: "Non",
  bajarildi: false,
  teglar: [],
  yaratildi: "2026-10-28T00:00:00.000Z",
  yangilandi: "2026-10-28T00:00:00.000Z",
  tartib: 1,
  versiya: 1,
  ...o,
});

Keyin shu tartibda: Soxtalar turi va soxtaOmbor, xizmatModuli, describe bloki (uch test), overrideProvider testi, kontekst, guard, pipe va interceptor testlari.

Kanon faylida qatorlar uzun; biz ularni telefon uchun bo'ldik va test nomlarini biroz qisqartirdik — ma'nosi o'zgarmadi. Formatlangan faylni loyiha nusxasida tekshirdik: npm run tip 0 xato, eslint 0, Vitest 7/7 («Natija» bo'limidagi chiqish).

Tekshiruv:

bash
npm run tip
npm run lint
npx vitest run test/birlik.test.ts

Butun to'plam (npm test) — keyingi darsda, e2e testlar bilan birga. Hali commit qilmang.

13. Real ishda

  • Nest loyihalarida har *.service.ts yonida *.service.spec.ts bo'ladi — nest g service uni o'zi yaratadi. Ko'p jamoada review'dagi birinchi savol: "spec bormi?".
  • Mock kutubxonalari: @golevelup/ts-vitest (yoki Jest uchun @golevelup/ts-jest) createMock<T>() beradi — Proxy asosida, xuddi kanondagi soxtaOmbor kabi. Biz ularni sinamadik; g'oyasi darsdagi bilan bir xil.
  • Ortiqcha mock xavfi: hamma narsa soxta bo'lgan test faqat "kod o'zini o'zi chaqiradi" degan narsani tekshiradi. Kanon qoidasi: mock — faqat o'z chegaramizda (ombor, tashqi HTTP), qolgani haqiqiy.
  • Intervyu: "Nest'da xizmatni qanday unit test qilasiz?", "useValue va overrideProvider farqi?", "Guard'ni ilovasiz qanday sinaysiz?".

Xulosa

  • Test.createTestingModule({...}).compile() — faqat DI konteyner; HTTP, middleware va global sozlamalar yo'q.
  • Sinalayotgan klass — haqiqiy, bog'liqliklari — useValue bilan soxta; vi.fn() chaqiruvlarni eslab qoladi.
  • overrideProvider (va overrideGuard, overridePipe …) — tayyor ro'yxatni compile() dan oldin almashtiradi.
  • Proxy soxtasi then ga undefined qaytarsin — aks holda Nest uni Promise deb chaqiradi.
  • Guard, pipe, interceptor — new + soxta ExecutionContext/ArgumentMetadata bilan alohida sinaladi.
  • Nest 12 sukuti — Vitest; kichik demoda Jest tezroq, lekin kanon ESM va bir xil vosita uchun Vitest'da qoldi.

Keyingi dars: NestJS e2e testlari — butun ilovani ko'tarib HTTP orqali sinaymiz, main.ts sozlamasini testda unutish tuzog'ini ko'ramiz va bu qadamni commit qilamiz.

Manbalar

  • NestJS hujjati — Migration guide (Jest va ESM: Node 24.9+, 2026-11-02 da o'qildi): https://docs.nestjs.com/migration-guide
  • NestJS hujjati — Testing (Test.createTestingModule, overrideProvider, overrideGuard): https://docs.nestjs.com/fundamentals/testing
  • Vitest hujjati — Mock functions (vi.fn, mockResolvedValueOnce): https://vitest.dev/api/mock
  • Kanon (IlmHamroh, 2026-10-28): test/birlik.test.ts, NestJS 12.2.0, Vitest 5.0.5; Vitest va Jest o'lchovi — kanon o'lchovi. then tuzog'i, unutilgan LOG xabari va 7/7 natija — biz ishga tushirdik (2026-11-02, Node 24.21.0).
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS unit testlari: Test.createTestingModule, soxta provider va overrideProvider — IlmHamroh