Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. Kanonga webhook qo'shildi
- 2.1 src/umumiy/webhook.ts
- 2.2 Jonli tekshiruv (kanon)
- 3. Usul 1: tikuv (fetchFn)
- 3.1 G'oya
- 3.2 Kanon testlari
- 4. Usul 2–4: kutubxonalar
- 4.1 undici MockAgent
- 4.2 MSW (Node)
- 4.3 nock
- 4.4 Taqqoslash
- 5. Lokal qabul qiluvchi va fixture'lar
- 5.1 Haqiqiy HTTP, lekin internetsiz
- 5.2 Yozib olingan fixture'lar
- 6. Tarmoqni butunlay bloklash
- 7. API qadami: webhook (holat 51, 5-qism)
- 8. Ko'p uchraydigan xatolar
- 8.1 Testda haqiqiy tashqi API
- 8.2 Faqat "muvaffaqiyat" holatini tekshirish
- 8.3 Global mock'ni tiklamaslik
- 8.4 Fire-and-forget xatosini yutish
- 8.5 Haqiqiy ko'rinishdagi domen
- 9. Mashqlar
- 1-mashq (oson): qaysi usul?
- 2-mashq (o'rta): timeout holati
- 3-mashq (qiyin): xavfsiz MockAgent o'rami
- 4-mashq: API qadami
- 10. Real ishda
- Xulosa
- Manbalar
Tashqi HTTP'ni mock qilish: fetch tikuvi, undici MockAgent, MSW va nock
Qisqacha: Testlar Telegram, Payme yoki boshqa tashqi API'ga haqiqiy so'rov yubormasligi kerak: sekin, beqaror, pullik va xavfli.
vazifalar-apiga webhook qo'shildi va kanon uni to'rt usulda soxtaladi. Hammasi ishladi. (1)fetchFntikuvi — 0 paket. (2) undiciMockAgent+setGlobalDispatcher— Node'ning ichkifetchi bilan ham. (3) MSW — 49 paket. (4) nock 15 — globalfetchni ham ushladi. Topilma:MockAgentyopilgandan keyin global dispatcher yopiq agentligicha qoldi va keyingi so'rovUND_ERR_DESTROYEDbilan yiqildi — global holat testlar orasida sizadi. Kanon tanlovi — tikuv.
Bu darsda
- Nega testlar internetga chiqmasligi kerakligini sanab bera olasiz.
vazifalar-apiwebhook'ini va unifetchFnorqali soxtalashni yozasiz.MockAgent, MSW va nock qayerda so'rovni ushlashini farqlaysiz.- Lokal qabul qiluvchi server va yozib olingan fixture'lardan foydalanasiz.
- Testda tarmoqni butunlay yopib qo'yishni bilasiz.
Oldin bilishingiz kerak: Built-in fetch va undici, MSW: API'ni tarmoq darajasida mock qilish, Mock, stub, spy va fake, Supertest.
1. Nega bu kerak?
Jasur aka talab qo'ydi: "Vazifa bajarildi deb belgilansa, jamoa Telegram guruhiga xabar ketsin." Telegram bot servisi keyinroq (29-qism) yoziladi. Hozir vazifalar-api faqat webhook yuboradi: sozlangan manzilga POST — "shu vazifa bajarildi". U yog'ini boshqa servis hal qiladi.
Webhook (webhook) — bir servis hodisa bo'lganda boshqa servisga o'zi HTTP so'rov yuborishi. "Teskari API": siz so'ramaysiz, sizga xabar keladi.
Endi testlar. Agar test haqiqiy manzilga so'rov yuborsa:
- sekin — tarmoq 100–2000 ms, test 5 ms;
- beqaror — internet uzilsa, test yiqiladi, kod esa to'g'ri;
- pullik yoki xavfli — SMS, to'lov, haqiqiy guruhga xabar;
- tekshirib bo'lmaydi — "server 500 qaytarsa nima bo'ladi?" holatini haqiqiy serverdan olib bo'lmaydi.
Hayotiy o'xshatish: yong'in mashqi. Haqiqiy yong'in chiqarilmaydi — signal chaladi, xodimlar chiqadi, vaqt o'lchanadi. Mashq haqiqiy xulqni tekshiradi, lekin hech narsa yonmaydi.
2. Kanonga webhook qo'shildi
2.1 src/umumiy/webhook.ts
WEBHOOK_URL sozlangan bo'lsa, vazifa bajarildi: false → true bo'lganda POST ketadi. Fayl to'liq (holat 51; qatorlar telefon uchun bo'lindi):
// webhook.ts — tashqi HTTP: vazifa bajarilganda WEBHOOK_URL ga POST
// (masalan Telegram bot serveri — 29-qism).
// Javobni kutmaydi (fire-and-forget), lekin xatoni yutmaydi — logga
// yozadi. fetch — tikuv (seam): testda soxtasi beriladi
import type { Log } from "./log.ts";
import type { Vazifa } from "../vazifalar/vazifa.ts";
export type Xabarchi = (
hodisa: "vazifa.bajarildi",
vazifa: Vazifa,
) => void;
export function webhookXabarchi(
url: string,
{
fetchFn = globalThis.fetch,
kutishMs = 2000,
log,
}: {
fetchFn?: typeof fetch;
kutishMs?: number;
log: () => Pick<Log, "warn" | "info">;
},
): Xabarchi {
return (hodisa, vazifa) => {
// so'rov konteksti (sorovId) — javob ketgandan keyin
// ham shu logger
const l = log();
fetchFn(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "vazifalar-api/1",
},
body: JSON.stringify({
hodisa,
vaqt: new Date().toISOString(),
vazifa: { id: vazifa.id, matn: vazifa.matn },
}),
signal: AbortSignal.timeout(kutishMs),
}).then(
(javob) => {
const m = {
hodisa,
vazifaId: vazifa.id,
status: javob.status,
};
if (javob.ok) l.info(m, "webhook yuborildi");
else l.warn(m, "webhook rad etildi");
// tanani o'qimaymiz — ulanish bo'shasin
void javob.body?.cancel();
},
(xato: unknown) => {
l.warn(
{ hodisa, vazifaId: vazifa.id, err: xato },
"webhook yuborilmadi",
);
},
);
};
}(Bitta joyda takrorlangan obyekt m ga olindi — ma'nosi aynan.) Uch qaror:
- Fire-and-forget — API javobi webhook'ni kutmaydi. Tashqi servis 2 soniya "o'ylasa", foydalanuvchi buni sezmaydi.
- Xato yutilmaydi —
catcho'rnidagi ikkinchithenargumentiwarnyozadi. Webhook ketmagani logda ko'rinadi (Correlation ID darsidagisorovIdbilan). fetchFn— tikuv: sukutglobalThis.fetch, testda soxtasi.
Xizmat (xizmat.ts) faqat false → true o'tishida chaqiradi — buning uchun avval eski holatni o'qiydi. server.ts: WEBHOOK_URL bo'lsa webhookXabarchi(config.WEBHOOK_URL, { log: () => joriyLog(log) }), bo'lmasa xabarchi yo'q.
2.2 Jonli tekshiruv (kanon)
Kanon lokal qabul qiluvchi server (4197) va API'ni (WEBHOOK_URL bilan) ishga tushirdi (tekshiruv-51.txt):
QABUL: POST /hook vazifalar-api/1 {"hodisa":"vazifa.bajarildi","vaqt":"2026-10-24T06:23:50.749Z","vazifa":{"id":1,"matn":"Hisobot yozish #ish"}}API logida — o'sha sorovId bilan:
{"daraja":"info","time":"2026-10-24T06:23:50.774Z","ilova":"vazifalar-api","sorovId":"kanon-51-0002","hodisa":"vazifa.bajarildi","vazifaId":1,"status":204,"msg":"webhook yuborildi"}3. Usul 1: tikuv (fetchFn)
3.1 G'oya
Mock, stub, spy va fake darsidagi tikuv: fetch ni parametr qilib oldik, testda boshqasini beramiz. Soddalashtirilgan nusxada sinaymiz — bitta soxta fetch javob beradi, ikkinchisi yiqiladi:
function webhookXabarchi(url, { fetchFn = globalThis.fetch, log }) {
return (hodisa, vazifa) => {
fetchFn(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hodisa, vazifa: { id: vazifa.id } }),
signal: AbortSignal.timeout(2000),
}).then(
(j) => log(`webhook yuborildi ${j.status}`),
(x) => log(`webhook yuborilmadi: ${x.message}`),
);
};
}
const chaqiruvlar = [];
const soxtaFetch = async (url, init) => {
chaqiruvlar.push([url, JSON.parse(init.body)]);
return new Response(null, { status: 204 });
};
const yiqiluvchi = async () => {
throw new TypeError("fetch failed");
};
const loglar = [];
const log = (m) => loglar.push(m);
const url = "https://hook.example.com/vazifa";
const hodisa = "vazifa.bajarildi";
webhookXabarchi(url, { fetchFn: soxtaFetch, log })(hodisa, { id: 7 });
webhookXabarchi(url, { fetchFn: yiqiluvchi, log })(hodisa, { id: 8 });
await new Promise((r) => setTimeout(r, 10));
console.log(JSON.stringify(chaqiruvlar));
console.log(JSON.stringify(loglar));Konsolda:
[["https://hook.example.com/vazifa",{"hodisa":"vazifa.bajarildi","vazifa":{"id":7}}]]
["webhook yuborildi 204","webhook yuborilmadi: fetch failed"]Hech qanday tarmoq yo'q — bu kod brauzerda ham ishlaydi («Ishga tushir»). Soxta fetch haqiqiy Response obyektini qaytaradi: new Response(tana, { status }) — Fetch standartining o'zi. setTimeout(…, 10) — fire-and-forget promise'lar tugashini kutish.
3.2 Kanon testlari
test/mock.test.ts ning webhook qismi (to'liq; qatorlar bo'lindi, manzil hook.example.com ga almashtirildi — sabab pastda):
const kut = (ms: number) => new Promise((r) => setTimeout(r, ms));
test("webhook: faqat false → true o'tishida, bitta marta; " +
"tana va URL to'g'ri (spy — mock.fn)", async (t) => {
// typeof fetch imzosi — chaqiruv argumentlari tipli bo'lsin
const soxtaFetch = t.mock.fn<typeof fetch>(() =>
Promise.resolve(new Response(null, { status: 204 })));
const { log, qatorlar } = xotiraLogi();
const xabarchi = webhookXabarchi("https://hook.example.com/v", {
fetchFn: soxtaFetch,
log: () => log,
});
const app = ilovaYarat({ ombor: new XotiraOmbori(), xabarchi });
await request(app).post("/v1/vazifalar")
.send({ matn: "Non" }).expect(201);
// bajarildi o'zgarmadi
await request(app).patch("/v1/vazifalar/1")
.send({ matn: "Non olish" }).expect(200);
await request(app).patch("/v1/vazifalar/1")
.send({ bajarildi: true }).expect(200);
// allaqachon true — qayta yuborilmaydi
await request(app).patch("/v1/vazifalar/1")
.send({ bajarildi: true }).expect(200);
await kut(20);
assert.equal(soxtaFetch.mock.callCount(), 1);
const [url, init] = soxtaFetch.mock.calls[0]?.arguments ?? [];
assert.equal(url, "https://hook.example.com/v");
assert.equal(init?.method, "POST");
assert.ok(typeof init.body === "string");
assert.deepEqual(
(JSON.parse(init.body) as { vazifa: unknown }).vazifa,
{ id: 1, matn: "Non olish" },
);
assert.ok(qatorlar.some(
(q) => q.msg === "webhook yuborildi" && q.status === 204,
));
});
test("webhook: tashqi server yiqilsa — javob baribir 200, " +
"xato logda (yutib yuborilmaydi)", async () => {
const { log, qatorlar } = xotiraLogi();
const xabarchi = webhookXabarchi("https://hook.example.com/v", {
fetchFn: () => Promise.reject(new TypeError("fetch failed")),
log: () => log,
});
const app = ilovaYarat({ ombor: new XotiraOmbori(), xabarchi });
await request(app).post("/v1/vazifalar")
.send({ matn: "a" }).expect(201);
await request(app).patch("/v1/vazifalar/1")
.send({ bajarildi: true }).expect(200);
await kut(20);
assert.ok(qatorlar.some(
(q) => q.msg === "webhook yuborilmadi" && q.daraja === "warn",
));
});Birinchi test to'rt PATCH holatini qamraydi va faqat bittasida webhook ketishini tekshiradi. Bu yerda chaqiruvlar sonini sanash — to'g'ri: "necha marta yuborildi" — talabning o'zi. Ikkinchi test eng muhim savolga javob beradi: tashqi servis yiqilsa, bizning API buzilmaydimi?
Diqqat: kanon testlari va tajribasi
https://hook.example.uz/…manzilini ishlatgan.example.uz— zaxiralangan domen emas, u kimgadir tegishli bo'lishi mumkin. Zaxiralangan nomlar —example.com,example.org,.invalid,.test(RFC 2606). Testda so'rov baribir chiqmaydi, lekin xato bilan chiqib ketsa — begona serverga boradi. Biz darsdaexample.comga almashtirdik.
4. Usul 2–4: kutubxonalar
Tikuv har doim ham mavjud emas: eski kod fetch ni ichida to'g'ridan chaqiradi yoki tashqi SDK (to'lov, pochta) o'z HTTP klientini ishlatadi. Unda so'rov pastroq qatlamda ushlanadi:
flowchart TD
K[Kod: fetchFn yoki fetch] -->|tikuv| T[soxta fetch — usul 1]
K --> F[global fetch]
F --> D[undici dispatcher]
D -->|MockAgent — usul 2| M[soxta javob]
F -->|msw / nock — usul 3, 4| I[fetch almashtiriladi]
D --> S[haqiqiy soket → internet]Diagrammaga qarang: usul qancha pastda bo'lsa, kodga shuncha kam tegadi, lekin global holatni shuncha ko'p o'zgartiradi.
Kanon tajribasi (t49-mock.txt) — o'sha webhookXabarchi ni tikuvsiz (sukut globalThis.fetch bilan) uchta kutubxona bilan sinadi. Har qatorda oxirgi qismga qarang — log nima dedi:
1. fetchFn tikuvi: 1 chaqiruv, tana {"hodisa":"vazifa.bajarildi","vaqt":"2026-10-24T06:08:07.381Z","vazifa":{"id":7,"matn":"Non olish"}}; log: info webhook yuborildi 204; 88.4 ms
2. undici 7.30.1 MockAgent (Node ichki undici 7.29.1): log: warn webhook rad etildi 500; pendingInterceptors: 0
3. msw: 1 so'rov ushlandi, hodisa vazifa.bajarildi; log: info webhook yuborildi 202
4. nock 15: scope bajarildi: true; log: info webhook yuborildi 200
5. .invalid manzil (mocksiz): log: warn webhook yuborilmadi UND_ERR_DESTROYEDNima bo'ldi? To'rt usul ham so'rovni ushladi va har biri o'z javobini berdi: 204, 500, 202, 200. Beshinchi qator esa mock'siz so'rov — u kutilmagan xato bilan yiqildi (sababi «MockAgent» bo'limida). Har birini ko'ramiz.
4.1 undici MockAgent
Node'ning fetch i ichida undici turadi (fetch va undici). Undici'da so'rovni kim yuborishi — "dispatcher" (agent). MockAgent — soxta dispatcher; uni global qilib qo'yasiz:
import { MockAgent, setGlobalDispatcher } from "undici";
const agent = new MockAgent();
agent.disableNetConnect(); // ro'yxatda yo'q so'rov — xato
agent
.get("https://hook.example.com")
.intercept({ path: "/vazifa", method: "POST" })
.reply(500, "xato");
setGlobalDispatcher(agent);
// … webhookXabarchi(…) — "webhook rad etildi 500"Muhim nozik joy: undici npm paketi va Node ichidagi undici — alohida nusxalar. Kanon npm'dagi 7.30.1 ni Node 24.21.0 ichidagi 7.29.1 bilan juftladi — ishladi. 2026-10-24 holatiga npm'dagi eng yangi undici — 8.12.3 (biz npm view bilan ko'rdik); katta versiya farq qilsa, setGlobalDispatcher Node'ning fetch iga ta'sir qilmasligi mumkin. Sinalmagan — o'rnatishda process.versions.undici ga mos asosiy versiyani tanlang.
Kanon topilmasi: test oxirida await agent.close() qilindi. Keyingi qism (.invalid manzil, mock'siz) — UND_ERR_DESTROYED. Ya'ni global dispatcher yopilgan MockAgent bo'lib qoldi va keyingi har fetch u orqali o'tib yiqildi. Haqiqiy test to'plamida bu — bir test boshqasini buzadi. Yechim: oldingi dispatcher'ni saqlab, testdan keyin qaytarish:
import { getGlobalDispatcher, setGlobalDispatcher } from "undici";
const oldingi = getGlobalDispatcher();
// … test …
setGlobalDispatcher(oldingi);
await agent.close();4.2 MSW (Node)
MSW ni frontend'da ko'rgansiz. Node'da setupServer bilan — xuddi shu handler'lar:
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
const server = setupServer(
http.post("https://hook.example.com/vazifa", async (h) => {
const { request } = h;
kelgan.push(await request.json());
return new HttpResponse(null, { status: 202 });
}),
);
server.listen({ onUnhandledRequest: "error" });
// … test …
server.close();onUnhandledRequest: "error" — handler'i yo'q so'rov xato beradi (tarmoqqa chiqmaydi). Narxi — 49 paket, 13.1 MB (t49-mock.txt). Frontend va backend bir xil handler'larni ishlatsa — foydali.
4.3 nock
nock — Node'ning eng eski HTTP mock kutubxonasi. Avval u faqat http/https modullarini ushlardi; global fetch (undici) uni chetlab o'tardi. Kanon nock 15.0.2 ni sinadi — global fetch ham ushlandi (scope bajarildi: true):
import nock from "nock";
const scope = nock("https://hook.example.com")
.post("/vazifa")
.reply(200);
nock.disableNetConnect();
// … webhookXabarchi(…) …
console.log(scope.isDone()); // true — kutilgan so'rov keldiscope.isDone() — "kutilgan so'rov haqiqatan keldimi" — mock (tor ma'noda) tekshiruvi. Narxi — 10 paket, 2.0 MB.
4.4 Taqqoslash
| Usul | Paketlar | Global holat | Qachon |
|---|---|---|---|
fetchFn tikuvi |
0 | yo'q | o'z kodingiz — har doim birinchi tanlov |
undici MockAgent |
1 (1.6 MB) | global dispatcher | tikuvsiz kod, faqat fetch |
| MSW | 49 (13.1 MB) | fetch almashtiriladi |
frontend bilan umumiy handler'lar |
| nock | 10 (2.0 MB) | http, https, fetch |
eski kod, http.request ishlatadigan SDK |
Kanon qarori: tikuv. Sabab — global holat yo'q (testlar parallel va bir-biriga ta'sirsiz), paket yo'q, kod o'qiganga aniq.
Tekshirib ko'ring: To'lov SDK'si ichida
https.requestishlatadi va siz uning kodiga tega olmaysiz. Qaysi usul?
Javob
nock — u http/https modullarini ushlaydi. MockAgent faqat undici (fetch) so'rovlarini ko'radi, SDK'ning https.request i uning yonidan o'tib ketadi. Yana bir yo'l — SDK'ning o'zini tikuv qilish: kodingizda SDK'ni bevosita emas, o'z kichik interfeysingiz (tolovKlienti.yarat(...)) orqali chaqirasiz va testda shu interfeysning fake'ini berasiz. Bu nock'dan ham ishonchliroq.
5. Lokal qabul qiluvchi va fixture'lar
5.1 Haqiqiy HTTP, lekin internetsiz
Ba'zan soxta fetch yetmaydi: haqiqiy sarlavhalar, tana kodlash, timeout xulqini tekshirish kerak. Unda testning o'zi kichik server ochadi va webhook'ni unga yo'naltiradi. Kanon jonli tekshiruvda aynan shunday qilgan (port 4197):
import { createServer } from "node:http";
// Lokal qabul qiluvchi: haqiqiy HTTP, lekin internet yo'q
const kelganlar = [];
const qabul = createServer(async (req, res) => {
let tana = "";
for await (const b of req) tana += b;
kelganlar.push(`${req.method} ${req.url} ${tana}`);
res.statusCode = 204;
res.end();
});
await new Promise((r) => qabul.listen(0, "127.0.0.1", r));
const url = `http://127.0.0.1:${qabul.address().port}/hook`;
const javob = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
hodisa: "vazifa.bajarildi",
vazifa: { id: 1 },
}),
});
console.log(javob.status);
console.log(kelganlar);
qabul.closeAllConnections();
qabul.close();Konsolda:
204
[ 'POST /hook {"hodisa":"vazifa.bajarildi","vazifa":{"id":1}}' ]Port 0, 127.0.0.1, test oxirida yopiladi — internet kerak emas, parallel testlar to'qnashmaydi.
5.2 Yozib olingan fixture'lar
Tashqi API javobi murakkab bo'lsa (CBU kurslari, to'lov holati), uni qo'lda yozish qiyin. Yechim — fixture (fixture): haqiqiy javobni bir marta yozib olib, faylda saqlash; testlar shu fayldan o'qiydi. 24-qismda valyuta aynan shunday qilgan: tarmoq bo'lmasa CBU javobining fixture'i.
nock'da buning tayyor rejimi bor — nock.back (birinchi yurishda yozadi, keyin faqat o'qiydi). Hujjat bo'yicha; kanonda sinalmagan. Ehtiyot bo'ling: yozib olingan javobda token, shaxsiy ma'lumot qolmasin — fixture git'ga tushadi.
6. Tarmoqni butunlay bloklash
Testlarni "tarmoqqa chiqmaydi" deb faraz qilish yetmaydi — majburlash kerak. Kimdir ertaga tikuvsiz fetch qo'shsa, test haqiqiy serverga boradi va hech kim sezmaydi.
| Vosita | Bloklash |
|---|---|
undici MockAgent |
agent.disableNetConnect() |
| MSW | server.listen({ onUnhandledRequest: "error" }) |
| nock | nock.disableNetConnect() (+ enableNetConnect("127.0.0.1")) |
| tikuv | sukut fetchFn ni testda hech qachon qoldirmaslik |
Node'ning --permission modeli bu yerda yordam bermaydi: Permission model darsida kanon topgan — Node 24 da u tarmoqni cheklamaydi. CI'da esa konteyner tarmoqsiz ishga tushirilishi mumkin — bu eng ishonchli devor (34-qism).
Kanon t49 ning 5-qismi qiziq bir holatni ham ko'rsatdi: .invalid (hech qachon mavjud bo'lmaydigan zaxiralangan TLD) manziliga so'rov mock'siz yuborildi — lekin u UND_ERR_DESTROYED bilan yiqildi. Sabab DNS emas, oldingi qismdan qolgan yopiq MockAgent edi («MockAgent» bo'limidagi topilma). Toza jarayonda bu so'rov DNS xatosi bilan yiqilishi kutiladi — biz alohida sinamadik.
7. API qadami: webhook (holat 51, 5-qism)
| Fayl | O'zgarish |
|---|---|
src/umumiy/webhook.ts |
yangi («src/umumiy/webhook.ts» bo'limi, to'liq) |
src/vazifalar/xizmat.ts |
3-parametr xabarchi?; yangila — false → true da xabarchi("vazifa.bajarildi", v) |
src/ilova.ts |
Bogliqliklar.xabarchi, vazifaXizmati(ombor, () => joriyLog(log), xabarchi) |
src/server.ts |
WEBHOOK_URL bo'lsa webhookXabarchi(…) |
test/mock.test.ts |
2 ta webhook testi («Kanon testlari») |
xizmat.ts dagi o'zgarish (kanon):
async function yangila(
id: number,
ozgarish: VazifaOzgarishi,
shart?: Shart,
): Promise<Vazifa> {
// Faqat false → true o'tishida xabar:
// avvalgi holatni bilish kerak
const avval =
xabarchi !== undefined && ozgarish.bajarildi === true
? await ombor.ol(id)
: undefined;
const v = natija(id, await ombor.yangila(id, ozgarish, shart));
if (
xabarchi !== undefined &&
avval?.bajarildi === false &&
v.bajarildi
) {
xabarchi("vazifa.bajarildi", v);
}
return v;
}server.ts ga bitta qator bogliqliklar ichida:
...(config.WEBHOOK_URL === undefined
? {}
: {
xabarchi: webhookXabarchi(config.WEBHOOK_URL, {
log: () => joriyLog(log),
}),
}),WEBHOOK_URL config'da holat 40 dan bor (ixtiyoriy URL). Hali commit qilmang. Bu kesimni ham nusxada tekshirdik: tip 0, lint 0, testlar 65/65 (o'tgan darsdagi 63 + 2 webhook testi).
8. Ko'p uchraydigan xatolar
8.1 Testda haqiqiy tashqi API
Sekin, beqaror, pullik. Tikuv yoki mock + tarmoqni bloklash.
8.2 Faqat "muvaffaqiyat" holatini tekshirish
Tashqi servis 500 bersa yoki javob bermasa API nima qiladi? Har tashqi chaqiruvga kamida uchta test: muvaffaqiyat, rad (4xx/5xx), tarmoq xatosi yoki timeout.
8.3 Global mock'ni tiklamaslik
MockAgent topilmasi: keyingi testlar sirli yiqiladi. getGlobalDispatcher() ni saqlab qaytarish yoki tikuv.
8.4 Fire-and-forget xatosini yutish
fetch(...).catch(() => {}) — webhook ketmasa hech kim bilmaydi. warn log (kanondagidek) va test.
8.5 Haqiqiy ko'rinishdagi domen
hook.example.uz — kimningdir domeni bo'lishi mumkin. example.com, .invalid, .test.
9. Mashqlar
1-mashq (oson): qaysi usul?
(a) O'z kodingiz, fetch parametr orqali keladi. (b) Eski modul, ichida https.get(...). (c) Frontend va backend bir xil soxta API'ni ishlatishi kerak. (d) Uchinchi tomon kutubxonasi fetch ni ichida chaqiradi, Node 24.
Yechim
(a) Tikuv — soxta fetchFn. (b) nock — https modulini ushlaydi. (c) MSW — handler'lar ikkala muhitda bir xil. (d) undici MockAgent (Node ichidagi undici bilan mos versiya) yoki nock 15; ikkalasida ham global holatni test oxirida tiklang.
2-mashq (o'rta): timeout holati
«G'oya» bo'limidagi soddalashtirilgan webhookXabarchi ga kutishMs parametrini qo'shing va javob bermaydigan soxta fetch yozing: u init.signal bekor qilinguncha kutsin va keyin signalning sababini (init.signal.reason) tashlasin. kutishMs: 50 bilan log nima bo'ladi?
Yechim
function webhookXabarchi(url, { fetchFn, log, kutishMs = 2000 }) {
return (hodisa) => {
fetchFn(url, {
method: "POST",
body: JSON.stringify({ hodisa }),
signal: AbortSignal.timeout(kutishMs),
}).then(
(j) => log(`yuborildi ${j.status}`),
(x) => log(`yuborilmadi: ${x.name}`),
);
};
}
// Javob bermaydigan server: faqat signal bekor qilganda to'xtaydi
const osilgan = (url, init) =>
new Promise((_, rad) => {
const s = init.signal;
s.addEventListener("abort", () => rad(s.reason));
});
const loglar = [];
const x = webhookXabarchi("https://hook.example.com/v", {
fetchFn: osilgan,
log: (m) => loglar.push(m),
kutishMs: 50,
});
x("vazifa.bajarildi");
await new Promise((r) => setTimeout(r, 100));
console.log(loglar.join("\n"));Konsolda:
yuborilmadi: TimeoutErrorAbortSignal.timeout(ms) muddat o'tganda TimeoutError nomli DOMException bilan bekor qiladi. Haqiqiy fetch ham aynan shu xatoni beradi. Kanonda 2000 ms — tashqi servis osilib qolsa ham API xotirasida cheksiz kutayotgan so'rovlar to'planmaydi.
3-mashq (qiyin): xavfsiz MockAgent o'rami
Kutubxonasiz model yozing: globalDispatcher o'zgaruvchisi (boshida "haqiqiy"), mockBilan(fn) funksiyasi — dispatcher'ni "mock" ga almashtirib fn ni bajarsin va xato bo'lsa ham asl holatni qaytarsin. Ikki holat bilan sinang: oddiy va xato tashlaydigan fn. Har biridan keyin dispatcher'ni chiqaring.
Yechim
let globalDispatcher = "haqiqiy";
async function mockBilan(fn) {
const oldingi = globalDispatcher;
globalDispatcher = "mock";
try {
return await fn();
} finally {
globalDispatcher = oldingi;
}
}
await mockBilan(async () => console.log("ichida:", globalDispatcher));
console.log("keyin:", globalDispatcher);
try {
await mockBilan(async () => {
throw new Error("test yiqildi");
});
} catch (x) {
console.log("xato:", x.message);
}
console.log("xatodan keyin:", globalDispatcher);Konsolda:
ichida: mock
keyin: haqiqiy
xato: test yiqildi
xatodan keyin: haqiqiyfinally — kanon topilmasining yechimi: test yiqilsa ham global holat tiklanadi. Haqiqiy kodda globalDispatcher = … o'rniga setGlobalDispatcher(agent) / setGlobalDispatcher(oldingi) va oxirida await agent.close().
4-mashq: API qadami
«API qadami» bo'limidagi o'zgarishlarni qiling va 2 ta webhook testini yozing (npm test). Keyin jonli sinang: «Lokal qabul qiluvchi» bo'limidagi serverni alohida faylda 4300 portda ishga tushiring, API'ni WEBHOOK_URL=http://127.0.0.1:4300/hook npm start bilan oching, vazifa yarating va uni bajarildi: true qiling. Qabul qiluvchida bitta POST ko'rinsinmi? Ikkinchi marta true yuborsangiz-chi? Commit qilmang.
Yechim
Birinchi PATCH — bitta POST (kanon QABUL: POST /hook vazifalar-api/1 {…} kabi; User-Agent sarlavhasi vazifalar-api/1). Ikkinchisi — yo'q: vazifa allaqachon true, false → true o'tishi bo'lmadi. Qabul qiluvchini to'xtatib PATCH qilsangiz — API baribir 200, logda "msg":"webhook yuborilmadi" (warn). Serverlarni Ctrl+C bilan to'xtating.
10. Real ishda
- To'lov tizimlari (Click, Payme, Stripe) ikki yo'nalishda HTTP: siz ularga so'rov yuborasiz va ular sizga webhook yuboradi. Ikkalasi ham testda soxtalanadi; Stripe'ning o'zi test rejimi va CLI bilan webhook yuborish vositasini beradi.
- Telegram bot API'si (29-qism) — tashqi HTTP; bot testlari
fetchFnyoki MSW bilan. - Kod sharhida: "bu test internetga chiqadimi?" — CI'da tarmoq yopilsa, javob avtomatik.
- Intervyu savollari: "Tashqi API'ni testda qanday soxtalaysiz?", "nock va MSW farqi?", "Webhook'ni qanday test qilasiz?", "Fire-and-forget xatolari bilan nima qilasiz?".
Xulosa
- Testlar tashqi tarmoqqa chiqmaydi: sekin, beqaror, pullik va xato holatlarni yasab bo'lmaydi.
- Kanon webhook: fire-and-forget, 2 s timeout, xato
warnlogda,fetchFn— tikuv. - To'rt usul ham ishladi: tikuv (0 paket), undici
MockAgent(ichki undici bilan mos versiya), MSW (49 paket), nock 15 (globalfetchni ham ushlaydi). MockAgentyopilgach global dispatcher yopiq qoldi —UND_ERR_DESTROYED; global holatnifinallyda tiklang.- Lokal qabul qiluvchi (port 0) — haqiqiy HTTP, internetsiz; fixture — yozib olingan javob, sirlarsiz.
- Tarmoqni majburan bloklang:
disableNetConnect,onUnhandledRequest: "error";--permissiontarmoqni cheklamaydi.
Keyingi dars: Vaqt va tasodifni boshqarish — mock.timers bilan Date ni to'xtatish va oldinga surish, UUID va tasodifni ukol qilish, seed'li faker va nega nomli import mock qilinmaydi.
Manbalar
- undici: MockAgent va Dispatcher.
- MSW: Node.js integration (3.x).
- nock: README (15.x) —
disableNetConnect,nock.back. - IETF: RFC 2606 — Reserved Top Level DNS Names (1999) —
example.com,.invalid,.test. - Kanon:
vazifalar-apiholat 51 (src/umumiy/webhook.ts,test/mock.test.ts),natijalar/tajriba/t49-mock.txt,natijalar/tekshiruv-51.txt.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!