IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API48/51-dars14 daqiqa
Mundarija (28)

Supertest bilan HTTP integration test: Express ilovasini portsiz tekshirish

Qisqacha: Supertest — Express ilovasiga haqiqiy HTTP so'rov yuborib, javobni zanjir bilan tekshiradigan kutubxona: await request(app).post("/v1/vazifalar").send({ matn: "Non" }).expect(201). Port ochish va yopish uning ishi — request(app) ilovani vaqtinchalik tasodifiy portda tinglatadi. Shart bitta: ilova faylida listen() bo'lmasin. Bo'lsa — biz sinaganimizda test o'tdi, lekin node --test tugamay osilib qoldi. Supertest fetch dan farqli ravishda Cache-Control: no-cache qo'shmaydi, shuning uchun 304 ni to'g'ri tekshiradi.

Bu darsda

  • Nega ilova va server alohida fayl bo'lishi kerakligini haqiqiy natija bilan ko'rsatasiz.
  • request(app) zanjiri bilan status, sarlavha va JSON tanani tekshirasiz.
  • Avtorizatsiya sarlavhasi va cookie agent bilan ishlaysiz.
  • Supertest xato xabarlarini o'qiy olasiz.
  • Vitest'da backend testlari uchun sozlamani bilasiz.

Oldin bilishingiz kerak: Backend test strategiyasi, node:test va node:assert, express.Router va qatlamli tuzilma, HTTP caching va shartli so'rovlar.

1. Nega bu kerak?

vazifalar-api ning integration testlari hozircha shunday yozilgan: before da server port 0 da ochiladi, testlar fetch yuboradi, after da server yopiladi. Bu ishlaydi — kanonda 33 ta test shunday. Lekin har faylda bir xil marosim: ochish, manzilni olish, yopish. Va fetch bilan bitta muammo bor — undan shartli so'rovlar darsida qoqilgansiz.

Supertest bu marosimni bitta chaqiruvga yig'adi. Hayotiy o'xshatish: avtomobil diagnostika stendi. Mashinani yo'lga olib chiqmasdan stendga qo'yasiz — g'ildiraklar aylanadi, dvigatel ishlaydi, asboblar hammasini o'lchaydi. Yo'l (haqiqiy port, haqiqiy mijoz) kerak emas, lekin mashinaning o'zi — haqiqiy.

2. Shart: listen() siz ilova

2.1 Kanondagi bo'linish

Qatlamli tuzilma darsida kanon ikki faylni ajratgan:

  • src/ilova.ts — ilovaYarat(bogliqliklar) Express ilovasini yasaydi va qaytaradi. listen() yo'q.
  • src/server.ts — config o'qiydi, ilovani yasaydi, listen() qiladi, signallarni ushlaydi.

Test ilova.ts ni import qiladi va har safar yangi ilova yasaydi. Server fayli testlarga umuman kirmaydi.

2.2 Bo'linmasa nima bo'ladi — sinadik

Ko'p darsliklarda app.js ichida app.listen(3000) yoziladi va testlar shu faylni import qiladi. Shuni taqlid qildik: test fayli modul darajasida server ochadi, test esa unga so'rov yuboradi.

js
import { test } from "node:test";
import { createServer } from "node:http";

// app.js ichidagi listen() kabi
const server = createServer((q, s) => s.end("ok")).listen(0);

test("ishlaydi", async () => {
  const { port } = server.address();
  const r = await fetch(`http://127.0.0.1:${port}`);
  if ((await r.text()) !== "ok") throw new Error();
});

Biz ishga tushirdik (node --test, Node 24.21.0). Test bir zumda o'tdi (✔ ishlaydi (22.3467ms)), lekin node --test tugamadi. 8 soniyadan keyin jarayonni majburan to'xtatdik — shundagina hisobot chiqdi va fayl yiqilgan deb belgilandi:

text
✔ ishlaydi (22.3467ms)
✖ ochiq.test.mjs (7986.2424ms)
ℹ tests 2
ℹ pass 1
ℹ fail 1

Sabab: ochiq server Node jarayonini tirik ushlab turadi (event loop darsi — faol handle bor ekan, jarayon chiqmaydi). --test-timeout=3000 ham yordam bermadi: u testni chegaralaydi, ochiq serverni emas. CI'da bu "testlar 6 soat osilib qoldi" degan xabar bilan tugaydi.

Port 3000 qat'iy bo'lsa, yana bir muammo: parallel ikki test fayli bir portni so'raydi — EADDRINUSE.

Tekshirib ko'ring: ilova.ts da listen() yo'q. Lekin ilova.ts ichida setInterval bilan har daqiqada eski idempotentlik kalitlarini tozalovchi taymer ishga tushirilsa-chi?

Javob

Xuddi shu muammo: faol taymer jarayonni tirik ushlaydi va test fayli tugamaydi. Yechimlar: taymerni server.ts da ishga tushirish; timer.unref() — taymer jarayonni ushlab turmasin; yoki to'xtatish funksiyasini qaytarib, testda after da chaqirish. Kanon idempotentlik omborida taymer ishlatmaydi — muddat ol() paytida tekshiriladi (o'tgan darsdagi TaymerOmbori kabi).

3. request(app): zanjir

3.1 O'rnatish va birinchi test

bash
npm i -D --save-exact supertest@7.3.1 @types/supertest@7.2.1

Kichik Express ilovasi bilan boshlaymiz (kanondan tashqari, faqat misol):

js
import express from "express";
import request from "supertest";

const app = express();
app.use(express.json());
const vazifalar = [];
app.post("/vazifalar", (req, res) => {
  if (typeof req.body?.matn !== "string") {
    return res.status(422).json({ xato: "matn kerak" });
  }
  const v = { id: vazifalar.length + 1, matn: req.body.matn };
  vazifalar.push(v);
  res.status(201).location(`/vazifalar/${v.id}`).json(v);
});

const r = await request(app)
  .post("/vazifalar")
  .send({ matn: "Non olish" })
  .expect(201);
console.log(r.status, r.headers.location, r.body);

Biz ishga tushirdik (express 5.3.0, supertest 7.3.1):

text
201 /vazifalar/1 { id: 1, matn: 'Non olish' }

Zanjirni o'qiymiz:

Qadam Nima qiladi
request(app) ilova tinglamayotgan bo'lsa — vaqtinchalik port 0 da ochadi
.post(yo'l) metod va yo'l
.send(obyekt) JSON tana, Content-Type: application/json o'zi qo'yiladi
.set(nom, qiymat) sarlavha
.expect(...) tekshiruv; mos kelmasa — xato
await javob: status, headers, body (JSON parse qilingan), text

r.body — tayyor obyekt; fetch dagi kabi await r.json() shart emas.

3.2 .expect() ning to'rt shakli va xato xabarlari

Ataylab noto'g'ri kutib ko'ramiz — xabarlar qanday ko'rinadi:

js
await request(app).post("/vazifalar").send({}).expect(201);
await request(app)
  .post("/vazifalar")
  .send({ matn: "a" })
  .expect(201, { id: 9, matn: "a" });
await request(app)
  .post("/vazifalar")
  .send({ matn: "b" })
  .expect("Content-Type", /xml/);

Biz ishga tushirdik — har biri try/catch ichida, e.message:

text
expected 201 "Created", got 422 "Unprocessable Entity"
expected { id: 9, matn: 'a' } response body, got { id: 2, matn: 'a' }
expected "Content-Type" matching /xml/, got "application/json; charset=utf-8"

Xabarlar aniq: nima kutildi, nima keldi. Shakllar:

  • .expect(201) — status.
  • .expect(201, tana) — status va tana (deepEqual kabi, to'liq moslik).
  • .expect("Sarlavha", qiymat yoki /regex/) — sarlavha.
  • .expect((res) => { … }) — o'z funksiyangiz; ichida assert ishlating.

.expect(201, { … }) to'liq tanani talab qiladi — yaratildi kabi o'zgaruvchan maydon bo'lsa yiqiladi. Kanon shuning uchun status va sarlavhalarni zanjirda, tanani esa alohida assert bilan tekshiradi.

4.1 .set() bilan sarlavha

Haqiqiy auth — 28-qismda. Hozir oddiy misol: /men faqat to'g'ri Authorization bilan javob beradi:

js
app.get("/men", (req, res) => {
  if (req.get("authorization") !== "Bearer sinov-token") {
    return res.sendStatus(401);
  }
  res.json({ ism: "Ali" });
});

await request(app).get("/men").expect(401);
const m = await request(app)
  .get("/men")
  .set("Authorization", "Bearer sinov-token")
  .expect(200);
console.log(m.body);

Biz ishga tushirdik:

text
{ ism: 'Ali' }

Ikkala holat ham — tokensiz 401, token bilan 200 — testda bo'lishi shart. Faqat "token bilan ishlaydi" ni tekshirgan test, himoya umuman o'chiq bo'lganda ham o'tadi.

request(app) har chaqiruvda yangi mijoz. Brauzer esa cookie'ni eslab qoladi. Buni request.agent(app) taqlid qiladi:

js
app.get("/sanoq", (req, res) => {
  const eski = req.headers.cookie?.match(/n=(\d+)/)?.[1] ?? 0;
  const n = Number(eski) + 1;
  res.cookie("n", String(n)).json({ n });
});

const agent = request.agent(app);
const a1 = await agent.get("/sanoq");
const a2 = await agent.get("/sanoq");
console.log(a1.body, a2.body);
console.log((await request(app).get("/sanoq")).body,
  (await request(app).get("/sanoq")).body);

Biz ishga tushirdik:

text
{ n: 1 } { n: 2 }
{ n: 1 } { n: 1 }

Agent ikkinchi so'rovda birinchi javobdagi Set-Cookie ni yubordi — sanoq oshdi. Oddiy request esa har safar "yangi brauzer". vazifalar-api da cookie yo'q (auth — 28-qism), lekin kanon agent testini qo'ygan: sessiya cookie'si qo'shilganda u tayyor turadi.

5. Kanon: test/supertest.test.ts

To'liq fayl (holat 51; uzun qatorlar telefon uchun bo'lindi, kod o'zgarmagan):

ts
// supertest.test.ts — HTTP integration test PORTSIZ: request(app)
// ilovani o'zi vaqtinchalik tinglatadi va yopadi.
// listen() ilova.ts da yo'qligi (holat 13) shuning uchun —
// app'ni to'g'ridan import qilamiz
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { ilovaYarat } from "../src/ilova.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";

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

test("supertest: yaratish → Location → o'qish", async () => {
  const app = yangiIlova();
  const yarat = await request(app)
    .post("/v1/vazifalar")
    .set("X-Request-Id", "supertest-0001")
    // obyekt — supertest o'zi JSON va Content-Type qo'yadi
    .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"');
  const tana = ol.body as { teglar: string[] };
  assert.deepEqual(tana.teglar, ["uy"]);
});

test("supertest: shartli so'rov — 304", async () => {
  const app = yangiIlova();
  await request(app)
    .post("/v1/vazifalar")
    .send({ matn: "a" })
    .expect(201);
  await request(app)
    .get("/v1/vazifalar/1")
    .set("If-None-Match", '"1"')
    .expect(304);
});

test("supertest: Problem Details va CORS preflight", async () => {
  const app = yangiIlova();
  const r = await request(app)
    .post("/v1/vazifalar")
    .send({ matn: "" })
    .expect(422)
    .expect("Content-Type", /application\/problem\+json/);
  assert.equal((r.body as { kod: string }).kod, "validatsiya");
  await request(app)
    .options("/v1/vazifalar/1")
    .set("Origin", "http://localhost:5173")
    .set("Access-Control-Request-Method", "PATCH")
    .expect(204)
    .expect("Access-Control-Allow-Origin", "http://localhost:5173");
});

test("supertest agent: ketma-ket so'rovlar", async () => {
  const agent = request.agent(yangiIlova());
  await agent.post("/v1/vazifalar")
    .send({ matn: "bir" }).expect(201);
  await agent.post("/v1/vazifalar")
    .send({ matn: "ikki" }).expect(201);
  const r = await agent.get("/v1/vazifalar?fields=matn").expect(200);
  const tana = r.body as { vazifalar: { matn: string }[] };
  assert.deepEqual(
    tana.vazifalar.map((v) => v.matn),
    ["bir", "ikki"],
  );
});

Test nomlari kanondan qisqartirildi; kod qatorlarga bo'lindi va ikki joyda oraliq tana o'zgaruvchisi qo'shildi — ma'nosi aynan. yangiIlova() — har test o'z ilovasi va omborini oladi. before/after yo'q: ochish-yopishni supertest qiladi.

Ikkinchi test muhim: fetch bilan u 200 berardi — undici If-None-Match bor so'rovga o'zi Cache-Control: no-cache qo'shadi va Express yangi javob qaytaradi (kanon topilmasi, holat 13). Shuning uchun kanon shartli so'rovlarni fetch bilan emas, node:http (xomSorov) yoki supertest bilan tekshiradi.

Mavjud fetch testlari o'zgartirilmadi — ikkalasi ham haqiqiy HTTP. Kanon qarori: yangi testlar supertest bilan.

sequenceDiagram
  participant T as test
  participant S as supertest
  participant A as ilova (Express)
  T->>S: request(app).post(...).send(...)
  S->>A: listen(0) — vaqtinchalik port
  S->>A: haqiqiy HTTP so'rov
  A-->>S: 201 + sarlavhalar + JSON
  S->>S: .expect() tekshiruvlari
  S->>A: server yopiladi
  S-->>T: javob (status, headers, body)

Diagrammaga qarang: so'rov haqiqiy HTTP orqali o'tadi — middleware, CORS, helmet, xato ishlovchi hammasi ishlaydi. Faqat port ochish va yopish yashirilgan.

Uchinchi testdagi POST /v1/vazifalar ({ "matn": "" }) ilova ichida qaysi yo'ldan o'tadi? src/ilova.ts dagi tartib bo'yicha:

Qadam Middleware Natija
1 sorovId() X-Request-Id yo'q — yangi UUID
2 sorovLogi(log) so'rov logi va sorovId konteksti
3 helmet() xavfsizlik sarlavhalari javobga qo'yiladi
4 cors(...) Origin yo'q — CORS sarlavhasi qo'shilmaydi
5 express.static public/ da bunday fayl yo'q — keyingisiga
6 express.json() tana o'qiladi: { matn: "" }
7 idempotentlik(...) Idempotency-Key yo'q — o'tkazib yuboradi
8 marshrut: jsonKerak, tekshirib(...) Zod: matn bo'sh → ValidatsiyaXatosi
9 xatoIshlovchi(...) 422, application/problem+json, kod: "validatsiya"

Supertest testi faqat 9-qadam natijasini ko'radi. Lekin unga yetish uchun oldingi sakkiz qadam haqiqatan bajarildi — integration testning qiymati shunda.

6. Vitest'da backend testlari

vazifalar-api node:test da qoldi (o'lchov). Lekin monorepo'da frontend bilan bitta vosita xohlasangiz — Vitest backend uchun ham ishlaydi. Kanon tajribasidagi sozlama (t46-runner.mjs, Vitest 5.0.5):

ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["vtest/**/*.test.ts"],
    environment: "node", // jsdom emas
    pool: "forks",
  },
});

environment: "node" — jsdom yuklanmaydi (backend'da window kerak emas). pool: "forks" — har fayl alohida jarayonda, node --test kabi (kanon o'lchovida threads biroz tezroq chiqdi: 3 044 va 3 265 ms). Supertest Vitest bilan o'zgarishsiz ishlaydi; node:assert ham. Faqat hook nomlari boshqa: before → beforeAll, after → afterAll.

7. API qadami: supertest (holat 51, 4-qism)

bash
npm i -D --save-exact supertest@7.3.1 @types/supertest@7.2.1
Fayl O'zgarish
test/supertest.test.ts yangi, 4 test («Kanon» bo'limi)
test/mock.test.ts idempotentlik spy testi — fetch o'rniga request(app)

Hali commit qilmang — holat 51 commit'i Coverage darsida. Bu kesimni biz holat 44 nusxasida tekshirdik (ikki paket, supertest.test.ts, supertest'li idempotentlik testi): tip 0, lint 0, testlar 63/63 (58 + 4 + 1).

8. Ko'p uchraydigan xatolar

8.1 listen() ilova faylida

Testlar o'tadi, jarayon osilib qoladi («Bo'linmasa nima bo'ladi» bo'limi). ilova.ts — yasaydi, server.ts — tinglaydi.

8.2 await siz zanjir

request(app).get("/").expect(200) — await siz so'rov ketmaydi yoki test uni kutmaydi. Har doim await.

8.3 .expect(status, tana) bilan o'zgaruvchan maydon

yaratildi vaqti har safar boshqa — test tasodifan yiqiladi. Statusni zanjirda, tanani alohida assert bilan (kerakli maydonlar).

8.4 .send("{...}") satr bilan

Satr yuborsangiz, supertest Content-Type ni application/x-www-form-urlencoded qo'yadi. Biz kanonda sinadik: vazifalar-api 415 Unsupported Media Type qaytardi. Obyekt bering yoki .set("Content-Type", "application/json").

8.5 Testlar orasida bitta ilova va ombor

Bir test qo'shgan vazifa ikkinchisining natijasini buzadi. Har test — yangiIlova().

9. Mashqlar

1-mashq (oson): xabarni o'qing

Supertest test yiqildi: expected "Location" of "/v1/vazifalar/1", got "/v1/vazifalar/3". Bu nimani bildiradi va eng ehtimolli sabab nima?

Yechim

Sarlavha bor, lekin qiymati boshqa: vazifa id'si 3 bo'lib chiqdi. Demak bu test ishlaganda ombor bo'sh emas edi — oldingi testlar ham shu omborga 2 ta vazifa qo'shgan. Sabab — testlar orasida umumiy ilova yoki ombor («Testlar orasida bitta ilova va ombor» xatosi). Yechim: har test o'z yangiIlova() si bilan.

2-mashq (o'rta): /teglar uchun testlar

vazifalar-api ning GET /v1/teglar endpoint'i [{ teg, soni }] ni ko'pidan kamiga qaytaradi. Supertest bilan test yozing: uchta vazifa ("Non #uy", "Sut #uy #bozor", "Hisobot #ish") yarating, keyin /v1/teglar → 200 va tana tekshiring. Tenglikda teg nomi bo'yicha tartib (kanon ombor.ts izohi).

Yechim
ts
test("teglar: ko'pidan kamiga, tenglikda alifbo", async () => {
  const app = yangiIlova();
  for (const matn of ["Non #uy", "Sut #uy #bozor", "Hisobot #ish"]) {
    await request(app)
      .post("/v1/vazifalar")
      .send({ matn })
      .expect(201);
  }
  const r = await request(app).get("/v1/teglar").expect(200);
  assert.deepEqual(r.body, {
    teglar: [
      { teg: "uy", soni: 2 },
      { teg: "bozor", soni: 1 },
      { teg: "ish", soni: 1 },
    ],
  });
});

Biz bu testni kanon holat 51 nusxasida yurgizdik — o'tdi, javob: {"teglar":[{"teg":"uy","soni":2},{"teg":"bozor","soni":1},{"teg":"ish","soni":1}]}. Tenglik qoidasi: bozor va ish ikkalasi 1 — alifbo bo'yicha bozor oldin. Tartibni tekshirish uchun aynan teng sonli teglar kerak — shuning uchun ikkitasi.

3-mashq (qiyin): o'z mini-supertest'ingiz

Supertest ichida nima bo'layotganini o'zingiz yozing: sora(ishlovchi, yol, init) — node:http server yaratadi, port 0 da tinglaydi, fetch yuboradi, har qanday holatda serverni yopadi va { status, tana } qaytaradi. Oddiy (req, res) ishlovchi bilan sinang (Express shart emas).

Yechim
js
import { createServer } from "node:http";
import { once } from "node:events";

// Mini-supertest: har so'rovga vaqtinchalik server (port 0)
async function sora(ishlovchi, yol, init) {
  const server = createServer(ishlovchi).listen(0, "127.0.0.1");
  await once(server, "listening");
  try {
    const { port } = server.address();
    const r = await fetch(`http://127.0.0.1:${port}${yol}`, init);
    return { status: r.status, tana: await r.text() };
  } finally {
    server.closeAllConnections();
    server.close();
  }
}

const app = (req, res) => {
  res.setHeader("Content-Type", "application/json");
  if (req.url === "/salomatlik") return res.end('{"holat":"ok"}');
  res.statusCode = 404;
  res.end('{"title":"Not Found"}');
};

console.log(await sora(app, "/salomatlik"));
console.log(await sora(app, "/yoq"));

Konsolda:

text
{ status: 200, tana: '{"holat":"ok"}' }
{ status: 404, tana: '{"title":"Not Found"}' }

finally — so'rov xato bilan tugasa ham server yopiladi. closeAllConnections() — fetch ochiq qoldirgan keep-alive ulanishlarini (keep-alive darsi) darhol uzadi. Biz uni olib tashlab ham sinadik: Node 24 da close() bo'sh turgan ulanishlarni o'zi yopdi va skript 0.7 s da tugadi. Lekin javob hali kelayotgan ulanish bo'lsa, close() uni kutadi — shuning uchun test yordamchisida closeAllConnections() ishonchliroq. Express ilovasi ham (req, res) funksiya — sora(expressApp, …) ham ishlaydi. Supertest bularning ustiga zanjir, .expect, agent va xato xabarlarini qo'shadi.

4-mashq: API qadami

supertest va @types/supertest ni o'rnating, test/supertest.test.ts ga «Kanon» bo'limidagi 4 testni yozing va test/mock.test.ts dagi idempotentlik testini request(app) ga o'tkazing. npm test — test soni oshganini tekshiring. Keyin bir testda ataylab .expect(200) ni .expect(201) ga o'zgartiring va xabarni o'qing. Commit qilmang.

Yechim

Kod — «Kanon» bo'limida va o'tgan darsdagi mock.test.ts parchasida. Ataylab buzilgan test xabari: expected 201 "Created", got 200 "OK". @types/supertest bo'lmasa, npm run tip TS7016: Could not find a declaration file for module 'supertest' beradi — shuning uchun ikkala paket birga.

10. Real ishda

  • Express loyihalari: Supertest — de-fakto standart; Jest yoki Mocha bilan ham, node:test va Vitest bilan ham bir xil.
  • NestJS (30-qism) e2e testlari ham request(app.getHttpServer()) — aynan Supertest.
  • Fastify o'z app.inject() usuliga ega — u tarmoqsiz, soxta so'rov bilan ishlaydi; g'oya bir xil.
  • Intervyu savollari: "Express ilovasini qanday integration test qilasiz?", "Nega app.listen alohida faylda?", "Supertest ichida nima bo'ladi?", "Cookie bilan testni qanday yozasiz?".

Xulosa

  • request(app) ilovani vaqtinchalik port 0 da ochadi, haqiqiy HTTP so'rov yuboradi va yopadi.
  • Shart: ilova faylida listen() yo'q; bo'lsa — test o'tadi, lekin node --test osilib qoladi (biz sinadik).
  • Zanjir: .post().set().send().expect(); .expect — status, sarlavha, tana yoki funksiya; xabarlar aniq.
  • .set("Authorization", …) — ikkala holatni ham (bor va yo'q) tekshiring; request.agent(app) — cookie'ni eslaydi.
  • Supertest no-cache qo'shmaydi — 304 testlari uchun fetch dan to'g'ri.
  • Vitest'da: environment: "node", pool: "forks"; supertest o'zgarishsiz ishlaydi.

Keyingi dars: Tashqi HTTP'ni mock qilish — vazifalar-api ga webhook qo'shamiz va uni to'rt usulda soxtalaymiz: fetchFn tikuvi, undici MockAgent, MSW va nock; testlar internetga chiqmasligini kafolatlash.

Manbalar

  • Supertest: README (7.x; repo ladjs/supertest dan ko'chgan, 2026-10-24 da ko'rildi).
  • Vitest: Test environment va Pools.
  • Kanon: vazifalar-api holat 51 (test/supertest.test.ts, test/mock.test.ts, src/ilova.ts), natijalar/tajriba/t46-runner.txt, QADAMLAR.md (holat 13 — fetch va 304).
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Supertest bilan HTTP integration test: Express ilovasini portsiz tekshirish — IlmHamroh