IlmHamroh
JavaScript Full-stack/30-qism. NestJS50/65-dars18 daqiqa
Mundarija (27)

NestJS e2e testlari: createNestApplication, supertest, overrideGuard va Testcontainers

Qisqacha: E2e (end-to-end) testda butun Nest ilovasi ko'tariladi va unga haqiqiy HTTP so'rov yuboriladi. Nest usuli: Test.createTestingModule({ imports: [AppModule] }) → createNestApplication() → init() → request(app.getHttpServer()) (supertest). Eng katta tuzoq — main.ts dagi sozlamalar (setGlobalPrefix, useGlobalPipes, middleware) testga o'zi o'tmaydi. Davosi — ilovani yasovchi bitta funksiya: main.ts ham, testlar ham uni chaqiradi. Har testdan keyin app.close(), baza esa har test oldidan toza holatga qaytariladi.

Bu darsda

  • Nest e2e testining to'rt qadamini yozasiz va unit testdan farqini aytasiz.
  • main.ts sozlamasi testda yo'qligi qanday xato berishini haqiqiy chiqishda ko'rasiz va oldini olasiz.
  • overrideGuard bilan himoyani testda almashtirasiz.
  • Testcontainers bilan haqiqiy Postgres'da test qanday ishlashini va har testni qanday tozalashni tushuntirasiz.
  • vazifalar-api ning butun test to'plamini yurgizib, unit testlar qadamini commit qilasiz.

Oldin bilishingiz kerak: NestJS unit testlari, Supertest bilan HTTP integration test, Testcontainers, Test DB strategiyalari, Global enhancer'lar va bajarilish tartibi.

O'tgan darsda bitta klassni soxta bog'liqliklar bilan sinadik. Bunday test tez, lekin u HTTP qatlamini umuman ko'rmaydi. Bugun teskari tomonga o'tamiz: hamma narsa haqiqiy, faqat tashqi chegaralar boshqariladi.

1. Nega bu kerak?

Unit test VazifalarService to'g'ri ishlashini ko'rsatdi. Lekin foydalanuvchi xizmatni chaqirmaydi — u POST /v1/vazifalar yuboradi. So'rov yo'lida ko'p narsa bor: so'rov ID middleware'i, CORS, JSON parser, guard, pipe, interceptor, xato filtri, versiyalash. Ularning birortasi noto'g'ri ulangan bo'lsa, unit testlar yashil qoladi, ilova esa ishlamaydi.

25-qismda bu xavfni HTTP integratsiya testlari bilan yopgandik (Supertest bilan HTTP integration test). Nest'ga ko'chishda ular o'zgarmadi — va aynan ular ko'chish to'g'ri ekanini isbotladi. Nest hujjati bunday testlarni e2e deb ataydi: ilovaning bir uchidan (HTTP so'rov) ikkinchi uchigacha (javob, baza).

O'xshatish: dvigatel stendda yaxshi ishladi (unit test). Endi mashinani to'liq yig'ib, yo'lga chiqarib sinaymiz — rul, tormoz, g'ildirak hammasi birga. Yo'l esa yopiq poligon: haqiqiy shahar ko'chasi emas, lekin haqiqiy asfalt. E2e testdagi baza — shu poligon.

2. Nest e2e testi: to'rt qadam

2.1 Shablondagi e2e

nest new shabloni test/app.e2e-spec.ts ni ham yaratadi:

ts
describe('AppController (e2e)', () => {
  let app: INestApplication<App>;

  beforeEach(async () => {
    const moduleFixture: TestingModule =
      await Test.createTestingModule({
        imports: [AppModule],
      }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });

  afterEach(async () => {
    await app.close();
  });
});
Qadam Nima qiladi
createTestingModule({ imports: [AppModule] }) butun ilova modul daraxti — unit testdagi kabi alohida klasslar emas
createNestApplication() HTTP adapter (Express) va ilova obyektini yasaydi
await app.init() marshrutlar, middleware, lifecycle hook'lar ulanadi; port ochilmaydi
request(app.getHttpServer()) supertest serverni vaqtincha tinglatib, so'rov yuboradi
app.close() onModuleDestroy hook'lari, pool va taymerlar yopiladi

Shablonda unit va e2e uchun ikkita alohida Vitest config bor: vitest.config.ts (**/*.spec.ts) va vitest.config.e2e.ts (**/*.e2e-spec.ts). npm run test:e2e ikkinchisini ishlatadi. vazifalar-api da esa 25-qismdagi nomlar saqlandi: hamma testlar test/*.test.ts, bitta config.

2.2 Unit va e2e yonma-yon

flowchart TB
  subgraph U["Unit test"]
    U1["Test.createTestingModule (providers)"] --> U2["VazifalarService"]
    U3["soxta ombor"] --> U2
  end
  subgraph E["E2e test"]
    E1["supertest so'rovi"] --> E2["middleware, guard, pipe"]
    E2 --> E3["controller → xizmat"]
    E3 --> E4["haqiqiy ombor: xotira yoki Postgres"]
    E3 --> E5["filtr → JSON javob"]
  end

Chap blokda — bitta klass va uning soxtasi. O'ng blokda — so'rovning butun yo'li. E2e testda soxta narsa kam: odatda faqat tashqi xizmatlar (webhook, email) va vaqt.

Tekshirib ko'ring: Nega app.init() port ochmaydi, lekin supertest baribir so'rov yubora oladi?

Javob

app.getHttpServer() Node'ning http.Server obyektini qaytaradi — hali tinglamayotgan. Supertest uni o'zi listen(0) bilan vaqtincha tasodifiy portga ulaydi, so'rov yuboradi va yopadi (Supertest bilan HTTP integration test). Shuning uchun testlar parallel ishlaganda port to'qnashmaydi.

3. Eng katta tuzoq: main.ts testga o'tmaydi

3.1 Tajriba

Kichik demo ilova qildik (biz ishga tushirdik: NestJS 12.2.0, Vitest 5.0.5, Node 24.21.0). main.ts da ilovaga global prefiks qo'yiladi — hamma marshrut /api/... bo'ladi:

ts
@Injectable()
export class SoatXizmati {
  hozir(): string {
    return new Date().toISOString();
  }
}

@Controller("salom")
export class SalomController {
  constructor(private readonly soat: SoatXizmati) {}

  @Get()
  ol() {
    return { xabar: "Salom, Toshkent!", vaqt: this.soat.hozir() };
  }

  @Post()
  yarat(@Body() tana: { ism?: string }) {
    return { salom: tana.ism ?? "mehmon" };
  }
}

@Module({ controllers: [SalomController], providers: [SoatXizmati] })
export class AppModule {}

// main.ts va testlar BIR XIL sozlamani chaqiradi
export function sozla(app: INestApplication): INestApplication {
  app.setGlobalPrefix("api");
  return app;
}

Birinchi test shablon uslubida — AppModule import qilinadi, lekin sozla() chaqirilmaydi:

ts
test("main.ts sozlamasi testda yo'q — 404", async () => {
  const m = await Test.createTestingModule({
    imports: [AppModule],
  }).compile();
  app = m.createNestApplication({ logger: false });
  await app.init();
  const r = await request(app.getHttpServer()).get("/api/salom");
  console.log("prefikssiz:", r.status, r.body.message);
});
text
prefikssiz: 404 Cannot GET /api/salom

Ilovada GET /api/salom ishlaydi. Testda esa 404. Sabab: createNestApplication() faqat modul daraxtini oladi. main.ts da app. bilan qilingan hamma narsa — setGlobalPrefix, useGlobalPipes, useGlobalFilters, enableVersioning, enableCors, app.use(...) — testda yo'q.

Bu misolda test yiqildi — yaxshi, darrov ko'rdik. Yomoni teskari holat: main.ts da app.useGlobalPipes(new ValidationPipe()) bor, testda yo'q. Test noto'g'ri tanani yuboradi, pipe ishlamaydi, controller uni qabul qiladi — va test o'tib ketadi. Ilovada esa boshqa xulq. Test ilovani emas, boshqa narsani sinagan bo'ladi.

3.2 Ikki davo

1-davo: enhancer'larni modulga ko'chirish. APP_PIPE, APP_GUARD, APP_FILTER, APP_INTERCEPTOR provider'lari modulning ichida turadi (Global enhancer'lar va bajarilish tartibi). Test AppModule ni import qilganda ular ham keladi. vazifalar-api da SxemaPipe, JsonTanaGuard, Problem filtri shu yo'l bilan ulangan.

2-davo: ilovani yasovchi bitta funksiya. Modulga sig'maydigan sozlamalar ham bor: Express middleware'lari (helmet, cors, JSON parser), versiyalash, Swagger. Ular uchun bitta funksiya yoziladi — main.ts ham, testlar ham uni chaqiradi. Demoda bu sozla():

ts
test("sozla() + overrideProvider — 200, vaqt qotirilgan",
  async () => {
    const m = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(SoatXizmati)
      .useValue({ hozir: () => "2026-11-02T09:00:00.000Z" })
      .compile();
    app = sozla(m.createNestApplication({ logger: false }));
    await app.init();
    const r = await request(app.getHttpServer())
      .get("/api/salom")
      .expect(200);
    console.log("sozla bilan:", JSON.stringify(r.body));
    await request(app.getHttpServer())
      .post("/api/salom")
      .send({ ism: "Malika" })
      .expect(201)
      .expect({ salom: "Malika" });
  });
text
sozla bilan: {"xabar":"Salom, Toshkent!","vaqt":"2026-11-02T09:00:00.000Z"}

Ikki narsaga qarang. Prefiks endi bor — 200. Vaqt esa qotirilgan: overrideProvider e2e testda ham ishlaydi. Soatni almashtirish — e2e'dagi kam sonli joiz soxtalardan biri (Vaqt va tasodifni boshqarish). POST uchun Nest sukut statusi 201 — .expect(201) shuni tekshiradi.

3.3 vazifalar-api da: ilovaYarat()

Kanon 2-davoni 25-qismdan beri ishlatadi. src/ilova.ts dagi ilovaYarat(b) ilovani to'liq yasaydi va listen() qilmaydi. main.ts uni chaqirib portni ochadi, testlar esa portsiz ishlatadi. Ichidagi tartib (qisqartirilgan parcha):

ts
export async function ilovaYarat(b: Bogliqliklar) {
  const app = await NestFactory.create<NestExpressApplication>(
    AppModule.forRoot({ /* ombor, config, log … */ }),
    { bodyParser: false, bufferLogs: true, abortOnError: false },
  );
  app.use(sorovId());
  // … log, CLS, helmet, cors, statik fayllar …
  app.use(express.json({ limit: "16kb", type: JSON_TURLARI }));
  app.use(tanaXatosi);
  app.enableVersioning({ type: VersioningType.URI /* … */ });
  hujjatniUla(app); // Swagger: /docs
  await app.init();
  return app;
}

Shuning uchun kanon createNestApplication() ni umuman ishlatmaydi: ilovaYarat() ning o'zi NestFactory.create ni chaqiradi. Testlar uchun kichik yordamchi bor — test/yordamchi.ts:

ts
/** supertest uchun: Nest ilovasi (init qilingan, listen
 * qilinmagan) http serveri */
export async function httpIlova(b: Bogliqliklar): Promise<Server> {
  return (await ilovaYarat(b)).getHttpServer();
}

Bogliqliklar ichida testlar ombor (new XotiraOmbori()), CORS ro'yxati, log va webhook xabarchisini o'zi beradi. Ya'ni "main.ts dagi ilova + test bergan chegaralar". «Global pipe'larni testda takrorlash» tuzog'i bu loyihada yo'q — takrorlanadigan narsaning o'zi yo'q.

Tekshirib ko'ring: main.ts da app.useGlobalFilters(new ProblemFilter()) yozilgan, e2e test shablon uslubida (createNestApplication()). Xato javoblari testda qanday ko'rinadi?

Javob

Filtr testda yo'q, shuning uchun xatolarni Nest'ning sukut filtri qayta ishlaydi: {"statusCode":404,"message":"…"} shaklida, application/problem+json emas. Problem Details'ni tekshiradigan test yiqiladi — yoki undan yomoni, bunday test umuman yozilmagan bo'lsa, farqni hech kim sezmaydi. Davo: filtrni APP_FILTER bilan modulga ko'chirish yoki ilovani bitta funksiyada yasash.

4. Supertest bilan vazifalar-api

25-qismdagi supertest testlari Nest'da o'zgarmasdan qoldi — faqat httpIlova() endi async. Birinchi testni o'qiymiz (qatorlar telefon uchun bo'lingan):

ts
const yangiIlova = () => httpIlova({
  ombor: new XotiraOmbori(),
  corsOriginlar: ["http://localhost:5173"],
});

test("supertest: yaratish → Location → o'qish", async () => {
  const app = await yangiIlova();
  const yarat = await request(app)
    .post("/v1/vazifalar")
    .set("X-Request-Id", "supertest-0001")
    .send({ matn: "Non olish #uy" })
    .expect(201)
    .expect("Content-Type", /application\/json/)
    .expect("X-Request-Id", "supertest-0001");
  const joy = String(yarat.headers.location);
  assert.equal(joy, "/v1/vazifalar/1");
  const ol = await request(app).get(joy).expect(200)
    .expect("ETag", '"1"');
  assert.deepEqual((ol.body as { teglar: string[] }).teglar, ["uy"]);
});

Bitta testda butun zanjir: so'rov ID middleware'i sarlavhani qaytardi, pipe tanani tekshirdi, xizmat teglarni ajratdi, interceptor ETag qo'ydi. Har test o'z ilovasini va o'z xotira omborini yaratadi — testlar orasida umumiy holat yo'q.

Fayldagi beshta testni yurgizdik (biz ishga tushirdik, vazifalar-api nusxasi):

text
 ✓ test/supertest.test.ts > supertest: yaratish → Location → o'qish (zanjir: .send().expect()) 329ms
 ✓ test/supertest.test.ts > supertest: shartli so'rov — 304 (supertest fetch emas: no-cache qo'shmaydi) 64ms
 ✓ test/supertest.test.ts > supertest: Problem Details va CORS preflight 70ms
 ✓ test/supertest.test.ts > supertest agent: bir xil ilova, ketma-ket so'rovlar (cookie bo'lsa saqlanardi — 28-qism) 62ms
 ✓ test/supertest.test.ts > CORS: config'dagi oxiri / li origin — Access-Control-Allow-Origin baribir chiqadi 46ms
 Test Files  1 passed (1)
      Tests  5 passed (5)

Ikkinchi test nomidagi izohga e'tibor bering: shartli so'rov (If-None-Match) fetch bilan emas, supertest bilan yuboriladi. fetch bunday so'rovga o'zi Cache-Control: no-cache qo'shadi va server 304 bermaydi — 25-qismdagi topilma.

5. overrideGuard — himoyani testda almashtirish

Ilovada himoyalangan marshrut bor deylik. Har e2e testda haqiqiy kirish jarayonini o'tish og'ir. Ba'zi testlar uchun guard'ni o'chirib qo'yish mumkin. Demoda x-kalit sarlavhasini talab qiladigan guard:

ts
@Injectable()
export class KalitGuard implements CanActivate {
  canActivate(k: ExecutionContext): boolean {
    const req = k.switchToHttp().getRequest<{
      headers: Record<string, string | undefined>;
    }>();
    return req.headers["x-kalit"] === "maxfiy";
  }
}

@Controller("hisobot")
@UseGuards(KalitGuard)
export class HisobotController {
  @Get()
  ol() {
    return { jami: 3 };
  }
}

Test ikki xil ilova yasaydi — haqiqiy guard bilan va almashtirilgan bilan:

ts
async function ilova(ochiq: boolean) {
  let b = Test.createTestingModule({ imports: [HisobotModule] });
  if (ochiq) {
    b = b.overrideGuard(KalitGuard)
      .useValue({ canActivate: () => true });
  }
  app = (await b.compile()).createNestApplication({ logger: false });
  return app.init();
}

Natija (biz ishga tushirdik):

text
haqiqiy: 403 {"message":"Forbidden resource","error":"Forbidden","statusCode":403}
almashtirilgan: 200 {"jami":3}

Guard false qaytarsa, Nest 403 beradi va o'z sukut xabarini yozadi: "Forbidden resource" — "taqiqlangan resurs". overrideGuard uni { canActivate: () => true } bilan almashtirdi — marshrut ochildi.

Diqqat: Guard'ni o'chirgan test himoyani sinamaydi. Har himoyalangan marshrut uchun kamida bitta test haqiqiy guard bilan bo'lsin: "kirmagan foydalanuvchi — 401", "boshqa rol — 403". Aks holda guard ulanmay qolsa ham hamma test yashil bo'ladi.

vazifalar-api da auth guard hali yo'q — u 28-qismdagi auth Nest'ga ko'chganda keladi (Global auth guard, @Public va @Roles). U yerda kanon overrideGuard o'rniga boshqa yo'lni tanlaydi: testlar haqiqiy sessiya bilan kiradi. Sabab yuqoridagi ogohlantirishda.

6. Haqiqiy baza: Testcontainers

6.1 Nima uchun

Xotira ombori tez, lekin u Postgres emas. Trigger, UNIQUE cheklovi, RLS, tranzaksiya — ular faqat haqiqiy bazada tekshiriladi. 27-qismda bunga Testcontainers ni o'rgangandik (Testcontainers): test o'zi Docker'da Postgres konteynerini ko'taradi, migratsiyalarni qo'llaydi va oxirida o'chiradi.

Nest'ga ko'chishda test/konteyner.test.ts o'zgarmadi — u omborni to'g'ridan sinaydi, Nest'ga tegmaydi. Kanon uni Nest ilovasi bilan bitta to'plamda yurgizdi. Boshlanishi:

ts
const YOQIQ = process.env["TESTCONTAINERS"] === "1";

if (!YOQIQ) {
  test.skip("Testcontainers — TESTCONTAINERS=1 bilan " +
    "ishga tushiriladi (Docker kerak)", () => {});
} else {
  const { PostgreSqlContainer } =
    await import("@testcontainers/postgresql");
  // …
  beforeAll(async () => {
    const k = await new PostgreSqlContainer("postgres:18")
      .withTmpFs({ "/var/lib/postgresql": "rw" })
      .start();
    // prisma migrate deploy — konteyner bazasiga
    await k.snapshot(); // migratsiyalangan bo'sh baza → andoza
  });
  // har testdan oldin: await konteyner.restoreSnapshot();
}

Bu — qisqartirilgan parcha: konteyner nomi, vaqt o'lchash va pool qatorlari olib tashlangan. Asosiy g'oya uch qatorda: konteyner ko'tarish, snapshot() (andoza) va har test oldidan restoreSnapshot().

Test ixtiyoriy (opt-in): oddiy npm test da u SKIP. Docker kerak, va har kungi tez tekshiruvni sekinlashtirmasligi kerak. TESTCONTAINERS=1 npm test bilan yoqiladi.

6.2 Kanon natijasi

Biz Docker'ni yoqmadik — chiqish kanon tekshiruvidan — loyihaning unit testlar qo'shilgan bosqichi. Qatorda uchta narsani toping: har bosqich vaqti, Tests 10 passed va oxirgi qism — testdan keyin qolgan konteynerlar soni:

text
--- Testcontainers (TESTCONTAINERS=1):  [Testcontainers vaqtlari] konteyner 2366 ms, migratsiya 1862 ms, snapshot 518 ms, restore (oxirgi) 236 ms  Tests 10 passed (10) ; kanon30-tc qoldiqlari: 0
Testcontainers: bir martalik va har test narxi
  • konteyner ko'tarishbir marta2 366 ms
  • migratsiyabir marta1 862 ms
  • snapshotbir marta518 ms
  • restorehar test oldidan236 ms

Manba: Kanon o'lchovi (IlmHamroh, 2026-10): unit testlar qo'shilgan bosqich tekshiruvi, postgres:18, bitta mashina

Grafikka qarang: qimmat qismlar — bir marta, fayl boshida. Har test faqat restore ni to'laydi. 27-qismda tozalash usullari o'lchangandi: TRUNCATE + ma'lumot ~100 ms, tranzaksiya + ROLLBACK ~19 ms, andozadan yangi baza ~350 ms (Test DB strategiyalari). Restore andoza usulining o'zi.

Oxirgi qismdagi "qoldiqlari: 0" — testdan keyin bitta ham konteyner qolmadi. afterAll da konteyner.stop() shuni ta'minlaydi.

6.3 Nega har test toza bazada

Bir test qo'shgan qator keyingi testga ta'sir qilsa — testlar tartibga bog'liq bo'ladi. Yakka o'tadi, birga yiqiladi, yoki teskarisi. Buni kichik modelda ko'ramiz (brauzerda ishlaydi):

js
// Testcontainers g'oyasi modeli: migratsiyadan keyingi holat —
// andoza, har testdan oldin undan toza nusxa
function bazaYarat() {
  let jadval = [];
  let andoza = null;
  return {
    qosh: (matn) => jadval.push({ id: jadval.length + 1, matn }),
    soni: () => jadval.length,
    snapshot: () => { andoza = structuredClone(jadval); },
    restore: () => { jadval = structuredClone(andoza); },
  };
}

const baza = bazaYarat();
baza.qosh("namuna: Non olish"); // "migratsiya + seed"
baza.snapshot();

const testlar = {
  "ikki vazifa qo'shiladi": () => {
    baza.qosh("Sut");
    baza.qosh("Choy");
    return baza.soni() === 3;
  },
  "bo'sh boshlanadi (faqat namuna)": () => baza.soni() === 1,
};

for (const tozalash of [false, true]) {
  console.log(tozalash ? "restore bilan:" : "tozalashsiz:");
  for (const [nom, fn] of Object.entries(testlar)) {
    if (tozalash) baza.restore();
    console.log(" ", fn() ? "✓" : "✗", nom);
  }
}

Konsolda:

text
tozalashsiz:
  ✓ ikki vazifa qo'shiladi
  ✗ bo'sh boshlanadi (faqat namuna)
restore bilan:
  ✓ ikki vazifa qo'shiladi
  ✓ bo'sh boshlanadi (faqat namuna)

Tozalashsiz ikkinchi test yiqildi — u birinchisining qatorlarini ko'rdi. restore bilan ikkalasi ham toza boshlanadi. structuredClone — chuqur nusxa (Sayoz va chuqur nusxalash). Model soddalashtirilgan: haqiqiy restoreSnapshot() Postgres'da bazani andozadan qayta yaratadi (CREATE DATABASE … TEMPLATE …).

7. app.close() va beqaror testlar

7.1 Nega yopish shart

app.init() ilova resurslarini ochadi: Postgres pool'i, Redis ulanishi, @Interval taymerlari (Rejalashtirish). Test tugagach ular ochiq qolsa — jarayon tugamaydi, yoki keyingi test ulanish chegarasiga uriladi. app.close() onModuleDestroy va onApplicationShutdown hook'larini chaqiradi (Lifecycle hook'lar) — vazifalar-api da SaqlashYopuvchi pool'ni shu yerda yopadi.

Demo testlarda afterEach(async () => { await app?.close(); }) — har testdan keyin. ?. — test ilovani yaratishga ulgurmay yiqilgan bo'lsa ham xato bermasligi uchun.

7.2 Yuk ostida timeout

Kanon tekshiruvida halol topilma bor. Birinchi yugurishda mashina boshqa og'ir ishlar bilan band edi: tip 63 s, lint 71 s oldi (odatda 5–10 s). Ikki test timeout bilan yiqildi: jarayon (60 s — bola jarayon ishga tushiradi) va prisma-ombor (20 s). Ikkinchi yugurishda hammasi o'tdi.

Bu kod xatosi emas, lekin muammo. Bola jarayon va baza bilan ishlaydigan testlar yuk ostida sekinlashadi. Kanon vitest.config.ts da sukut 5 s ni 20 s ga oshirgan (testTimeout: 20_000), hook'lar uchun 30 s. Beqaror (flaky) testni "qayta yurgiz, o'tadi" deb qoldirish xavfli: haqiqiy xato ham shunday ko'rinadi (Test strategiyasi va flaky testlar).

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Testda 404, ilovada ishlaydi setGlobalPrefix/versiyalash faqat main.ts da ilovani bitta funksiyada yasang
Testda validatsiya ishlamaydi useGlobalPipes faqat main.ts da APP_PIPE yoki umumiy funksiya
Xato javobi shakli boshqacha filtr useGlobalFilters bilan APP_FILTER
Vitest tugamaydi yoki "pool to'ldi" app.close() yo'q afterEach/afterAll da yoping
Testlar yakka o'tadi, birga yiqiladi umumiy baza holati har test oldidan tozalash
Guard o'chirilgan, himoya sinalmagan faqat overrideGuard li testlar haqiqiy guard bilan 401/403 testlari

9. Mashqlar

Mashqlar kurs/mashqlar/30/50-e2e-testlar/ da.

1-mashq (oson): qaysi biri testga o'tadi?

main.ts da yozilgan va modulda yozilgan sozlamalar. Shablon uslubidagi e2e testda (createNestApplication()) ular bormi?

  • app.setGlobalPrefix("api") —
  • { provide: APP_PIPE, useClass: SxemaPipe } (AppModule da) —
  • app.useGlobalFilters(new ProblemFilter()) —
  • @UseGuards(KalitGuard) (controller'da) —
Yechim
  • yo'q — app. bilan qilingan hamma narsa ilova obyektida, modulda emas.
  • bor — APP_PIPE — modul provider'i, AppModule bilan keladi.
  • yo'q — useGlobalFilters ham app. usuli.
  • bor — dekorator controller klassida, modul bilan birga keladi.

2-mashq (o'rta): bitta sozlash funksiyasi

Paketsiz model. ilovaYasa() marshrutlar jadvalini qaytarsin: { "/salom": () => "Salom" }. sozla(ilova, prefiks) hamma kalitlarga prefiks qo'shgan yangi obyekt qaytarsin. sorov(ilova, yol) — topilsa 200 <javob>, topilmasa 404. Ikki "test" yozing: biri sozla siz, biri bilan; ikkalasi ham /api/salom so'rasin. Ishora: Object.entries va Object.fromEntries (Obyekt metodlari).

Yechim
js
function ilovaYasa() {
  return { "/salom": () => "Salom" };
}

function sozla(ilova, prefiks) {
  return Object.fromEntries(
    Object.entries(ilova).map(([yol, f]) => [prefiks + yol, f]),
  );
}

function sorov(ilova, yol) {
  const f = ilova[yol];
  return f ? `200 ${f()}` : "404";
}

// "main.ts": sozla(ilovaYasa(), "/api")
const testlar = {
  "sozlasiz test": ilovaYasa(),
  "sozla bilan test": sozla(ilovaYasa(), "/api"),
};
for (const [nom, ilova] of Object.entries(testlar)) {
  console.log(`${nom}: ${sorov(ilova, "/api/salom")}`);
}

Konsolda:

text
sozlasiz test: 404
sozla bilan test: 200 Salom

Model darsdagi haqiqiy tajribani takrorlaydi: modul (ilovaYasa) bir xil, farq faqat sozlamada. main.ts ham sozla ni chaqirsa, test va ilova bir xil bo'ladi.

3-mashq (qiyin): resurs yopilmasa

Paketsiz model. ilovaOch() "ochiq ulanishlar" hisoblagichini (ochiq) bittaga oshirsin va { yop() } qaytarsin — yop() hisoblagichni kamaytirsin, ikkinchi marta chaqirilsa hech narsa qilmasin. Uchta "test" yurgizing: birinchi va uchinchisi yop() ni finally da chaqirsin, ikkinchisi chaqirmasin va xato tashlasin. Oxirida ochiq ni chiqaring. Keyin ikkinchi testni ham finally bilan tuzating. Ishora: try/finally (try, catch, finally).

Yechim
js
let ochiq = 0;

function ilovaOch() {
  ochiq++;
  let yopildi = false;
  return {
    yop() {
      if (yopildi) return;
      yopildi = true;
      ochiq--;
    },
  };
}

function yurgiz(nom, fn, yopish) {
  const app = ilovaOch();
  try {
    fn();
    console.log("✓", nom);
  } catch (e) {
    console.log("✗", nom, "—", e.message);
  } finally {
    if (yopish) app.yop();
  }
}

const xatoli = () => {
  throw new Error("kutilmagan 500");
};
yurgiz("birinchi", () => {}, true);
yurgiz("ikkinchi (yopilmaydi)", xatoli, false);
yurgiz("uchinchi", () => {}, true);
console.log("ochiq ulanishlar:", ochiq);

ochiq = 0;
yurgiz("ikkinchi (finally bilan)", xatoli, true);
console.log("tuzatilgandan keyin:", ochiq);

Konsolda:

text
✓ birinchi
✗ ikkinchi (yopilmaydi) — kutilmagan 500
✓ uchinchi
ochiq ulanishlar: 1
✗ ikkinchi (finally bilan) — kutilmagan 500
tuzatilgandan keyin: 0

Yiqilgan test ham resursni yopishi kerak — shuning uchun yopish finally da (Vitest'da — afterEach). Bitta yopilmagan ilova haqiqiy loyihada bitta ochiq pool degani: Vitest jarayoni tugamay osilib turadi.

10. Loyiha qadami: test to'plami va commit

O'tgan darsda test/birlik.test.ts qo'shildi. E2e uchun yangi fayl kerak emas: 25–27-qismlardagi HTTP testlari (api, rest, supertest, toplam, idempotentlik …) aynan e2e vazifasini bajaradi va ilovaYarat() orqali haqiqiy Nest ilovasini ko'taradi. Testcontainers — ixtiyoriy.

Butun to'plamni yurgizing:

bash
npm run tip
npm run lint
npm test
npm run build
TESTCONTAINERS=1 npm test   # Docker bo'lsa

Kanon natijasi (bazali testlar bilan, Docker'dagi Postgres 18.6):

text
--- npm run tip
exit 0 (6181 ms)
--- npm run lint
exit 0 (13926 ms)
--- npm run test
 ↓ test/konteyner.test.ts (1 test | 1 skipped)
 Test Files  25 passed | 1 skipped (26)
      Tests  130 passed | 1 skipped (131)
exit 0 (14776 ms)
--- npm run build
exit 0 (6690 ms)

130 ta test o'tdi, bitta SKIP — Testcontainers (opt-in). Uning alohida natijasi «Kanon natijasi» bo'limidagi: 10/10. Lokal bazangiz yo'q bo'lsa, bazali testlar (pg-ombor, prisma-ombor, migratsiya) o'zi SKIP bo'ladi — npm test yiqilmaydi.

Hammasi yashil bo'lsa — commit:

bash
git add test/birlik.test.ts
M="test: Nest unit testlari — TestingModule + vi.fn mock'lar,"
M+=" overrideProvider, guard/pipe/interceptor alohida"
git commit -m "$M"

11. Real ishda

  • Nest loyihalarida e2e testlar odatda test/*.e2e-spec.ts da, alohida config bilan va CI'da alohida bosqich sifatida yuradi. Sababi — ular sekinroq va baza talab qiladi.
  • "Ilova fabrikasi" (bizdagi ilovaYarat) — katta jamoalarda keng tarqalgan naqsh: createApp() yoki bootstrapApp() nomi bilan. Kod review'da "bu sozlama main.ts da qoldi, testda yo'q" — tez-tez uchraydigan izoh.
  • Testcontainers CI'da Docker talab qiladi; GitHub Actions'ning Linux runner'larida Docker bor (33–34-qismlarda CI va Docker'ni o'rganamiz).
  • Intervyu: "Unit va e2e test farqi?", "E2e testda nega validatsiya ishlamay qolishi mumkin?", "Testlar orasida bazani qanday tozalaysiz?".

Xulosa

  • E2e: createTestingModule({ imports: [AppModule] }) → createNestApplication() → init() → supertest; port ochilmaydi.
  • main.ts dagi app. sozlamalari testga o'tmaydi; davosi — APP_* provider'lar va ilovani yasovchi bitta funksiya.
  • vazifalar-api da ilovaYarat() — main.ts va testlar uchun bitta; 25–27 HTTP testlari Nest'da o'zgarmay e2e bo'ldi.
  • overrideProvider/overrideGuard e2e'da ham ishlaydi, lekin himoya haqiqiy guard bilan ham sinalsin.
  • Testcontainers: qimmat qism bir marta, har test oldidan restoreSnapshot(); har testdan keyin app.close().

Keyingi dars: Fastify adapter bilan NestJS — Express o'rniga Fastify: bir xil endpoint besh xil serverda o'lchanadi va kanon nega Express'da qolgani ko'rsatiladi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS e2e testlari: createNestApplication, supertest, overrideGuard va Testcontainers — IlmHamroh