Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Uch yo'l
- 3. Soxta soat: await tuzog'i
- 3.1 Eslatma
- 3.2 Haqiqiy Vitest chiqishi
- 3.3 Nega shunday
- 3.4 Soxta soat yoki soat parametri?
- 4. Navbat va cron: ishlovchini to'g'ridan chaqirish
- 4.1 BullMQ ishchisi
- 4.2 Butun zanjir: haqiqiy Redis bilan
- 4.3 Cron ishi
- 5. Webhook imzosi va vaqt
- 6. Email va realtime: tashqi tizimni kutib tekshirish
- 6.1 Mailpit
- 6.2 SSE va WebSocket
- 7. Kanon topgan hodisa: "javob keldi" ≠ "ish tugadi"
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): kesh muddati soxta soat bilan
- 3-mashq (qiyin): o'z kutib funksiyangiz
- 10. Loyiha qadami: test/asinxron.test.ts
- 11. Real ishda
- Xulosa
- Manbalar
Asinxron komponentlarni testlash: soxta soat, navbat, cron, webhook va email testlari
Qisqacha: Asinxron komponent — natijasi "keyinroq" chiqadigan kod: navbatdagi ish, kechasi ishlaydigan cron, 30 soniyalik kesh, 5 soniyadan keyingi qayta urinish, kelgan email. Uni testlashning uchta yo'li bor: ishlovchini to'g'ridan chaqirish (navbat va jadvalsiz), soxta soat (vaqtni test suradi) va kutib tekshirish (haqiqiy Redis, Mailpit bilan — natija chiqguncha qisqa oraliqda so'rab turish). Kanonning yangi
asinxron.test.tsfayli 30 soniyalik keshni va 5 soniyalik qayta urinishni soxta soat bilan millisekundlarda tekshiradi. Asosiy tuzoq:awaitbor kodda vaqtniadvanceTimersByTimeemas,advanceTimersByTimeAsyncbilan surish kerak.
Bu darsda
- Asinxron komponentni testlashning uch yo'lini farqlaysiz va har biriga misol keltirasiz.
advanceTimersByTimevaadvanceTimersByTimeAsyncfarqini haqiqiy Vitest chiqishida ko'rasiz.- BullMQ ishchisini, cron ishini va webhook imzosini qanday testlashni bilasiz.
- Email va realtime hodisalarni "kutib tekshirish" bilan, beqaror testsiz tekshirasiz.
vazifalar-apigatest/asinxron.test.tsni qo'shasiz.
Oldin bilishingiz kerak: Vitest II: mock, spy, taymerlar va coverage, Vaqt va tasodifni boshqarish, BullMQ asoslari, Test strategiyasi va flaky testlar.
31-qismning so'nggi to'rt darsi — test haqida. Prodakshn naqshlarini qurdik: kesh, kvota, qayta urinish, cron, navbat. Endi ularni ishonchli tekshiramiz. Bu darsda vaqtga va fon jarayonlariga bog'liq kod, keyingilarida — API shartnomasi, yuklama va butun test tizimi.
1. Nega bu kerak?
vazifalar-api da "keyinroq" bo'ladigan ishlar ko'p:
- muddat yaqinlashsa, navbatdagi ish 30 daqiqa oldin eslatma yuboradi;
- har kuni 04:15 da ishchi eski ma'lumotni tozalaydi;
- feature flag'lar 30 soniya keshda turadi;
- AI provayderi
429qaytarsa,Retry-After: 5ga ko'ra 5 soniyadan keyin qayta uriniladi; - Telegram bot dialog holatini Redis'ga yozadi.
Oddiy test bularning hammasida qoqiladi. Haqiqiy 5 soniya kutsangiz, 340 ta testli to'plam soatlab yuradi. Kutmasangiz, natijani tekshira olmaysiz. "Taxminan 100 ms kutamiz" desangiz, test yuklangan mashinada yiqiladi — beqaror (flaky) test tug'iladi.
O'xshatish: oshxonada palov 40 daqiqa damlanadi. Oshpazni tekshiruvchi inspektor 40 daqiqa kutib o'tirmaydi. U yo retseptni o'qiydi (ishlovchini to'g'ridan chaqirish), yo soatni o'zi buradi (soxta soat), yo har 5 daqiqada qozonga qaraydi va tayyor bo'lganda yozadi (kutib tekshirish). Uchalasi ham to'g'ri — vaziyatga qarab.
2. Uch yo'l
flowchart TB
S{"Nimani tekshiramiz?"}
S -->|"ishning mantig'i"| A["Ishlovchini to'g'ridan chaqirish"]
S -->|"vaqt: TTL, backoff"| B["Soxta soat"]
S -->|"haqiqiy tashqi tizim"| C["Kutib tekshirish"]
A --> A1["eslatmaIshlovi(d)(ish)"]
B --> B1["advanceTimersByTimeAsync"]
C --> C1["kutib: har 50 ms, chegara 8 s"]Diagrammani yuqoridan o'qing: birinchi savol — "aynan nimani tekshiryapman?". Ishning ichidagi mantiq bo'lsa — navbat ham, Redis ham kerak emas. Vaqtning o'zi (kesh muddati, kutish) bo'lsa — soxta soat. Navbat haqiqatan ishni ishchiga yetkazadimi, email haqiqatan jo'nadimi — buni faqat haqiqiy tizim va kutib tekshirish ko'rsatadi.
| Yo'l | Tezlik | Nimani isbotlaydi | Nimani isbotlamaydi |
|---|---|---|---|
| Ishlovchini chaqirish | millisekund | mantiq to'g'ri | navbat, jadval, ulanish |
| Soxta soat | millisekund | vaqt chegaralari aniq | haqiqiy I/O |
| Kutib tekshirish | soniyalar | butun zanjir ishlaydi | aniq vaqt chegarasi |
Kanon uchalasini ham ishlatadi. Keling, har birini kanondagi haqiqiy test bilan ko'ramiz.
3. Soxta soat: await tuzog'i
3.1 Eslatma
Soxta taymerlarni Vitest II darsida debounce bilan, Vaqt va tasodifni boshqarish darsida mock.timers bilan ishlatgansiz. U yerdagi kod sinxron edi: taymer qo'yildi, vaqt surildi, callback ishladi. Backend kodi esa deyarli doim async. Qayta urinishning eng oddiy shakli:
const kut = (ms: number) =>
new Promise((ok) => setTimeout(ok, ms));
// Birinchi urinish yiqiladi, 5 s kutib ikkinchisi o'tadi
async function qaytaUrin(ish: () => Promise<string>) {
try {
return await ish();
} catch {
await kut(5000);
return ish();
}
}Shu funksiyani soxta soat bilan ikki xil testlaymiz: vaqtni advanceTimersByTime (sinxron) va advanceTimersByTimeAsync (asinxron) bilan suramiz.
3.2 Haqiqiy Vitest chiqishi
function ishYasa() {
let n = 0;
return () => (++n === 1
? Promise.reject(new Error("429"))
: Promise.resolve("ok"));
}
beforeEach(() => { vi.useFakeTimers(); });
afterEach(() => { vi.useRealTimers(); });
test("sinxron surish: taymer hali qo'yilmagan", async () => {
let tugadi = false;
void qaytaUrin(ishYasa()).then(() => { tugadi = true; });
vi.advanceTimersByTime(5000);
await Promise.resolve();
console.log("navbatdagi taymerlar:", vi.getTimerCount());
expect(tugadi).toBe(true);
});
test("asinxron surish: mikrovazifalar ham bajariladi", async () => {
let tugadi = false;
void qaytaUrin(ishYasa()).then(() => { tugadi = true; });
await vi.advanceTimersByTimeAsync(5000);
console.log("navbatdagi taymerlar:", vi.getTimerCount());
expect(tugadi).toBe(true);
});Chiqishda ikki narsaga qarang: har testdagi navbatdagi taymerlar soni va pastdagi × / ✓ belgilari. Biz ishga tushirdik (Vitest 5.0.5, Node 24.21.0; chiqish qisqartirilgan):
stdout | test/demo31.test.ts > sinxron surish: taymer hali qo'yilmagan
navbatdagi taymerlar: 1
stdout | test/demo31.test.ts > asinxron surish: mikrovazifalar ham bajariladi
navbatdagi taymerlar: 0
× |asosiy| test/demo31.test.ts > sinxron surish: taymer hali qo'yilmagan 14ms
→ expected false to be true // Object.is equality
✓ |asosiy| test/demo31.test.ts > asinxron surish: mikrovazifalar ham bajariladi 2msBirinchi test 5 000 ms surdi, lekin ish tugamadi va navbatda bitta taymer qoldi. Ikkinchisida navbat bo'sh, ish tugagan.
3.3 Nega shunday
Vaqtni surgan lahzada kut(5000) taymeri hali qo'yilmagan edi. Ketma-ketlikni kuzating:
qaytaUrinchaqirildi,ish()rad etilgan Promise qaytardi.catchbloki darhol emas, mikrovazifa (microtask) sifatida ishlaydi — joriy sinxron kod tugagach (Microtask va macrotask chuqur darsidagi navbat).vi.advanceTimersByTime(5000)esa sinxron: u soatni shu zahoti suradi. Bu paytda navbatda birorta taymer yo'q — surish hech narsani ishga tushirmaydi.- Keyin
catchishlaydi vasetTimeout(ok, 5000)qo'yadi — endi yangi soat bo'yicha, yana 5 soniya keyinga.
advanceTimersByTimeAsync har taymerdan oldin va keyin mikrovazifalarga navbat beradi. catch ulguradi, taymer qo'yiladi, keyin surish uni ishga tushiradi.
Buni paketsiz modelda ko'ramiz. SoxtaSoat — Vitest soxta soatining juda soddalashtirilgan nusxasi: taymerlar ro'yxati va ikki xil "surish":
const haqiqiyTimeout = setTimeout;
class SoxtaSoat {
hozir = 0;
navbat = [];
setTimeout(fn, ms) {
this.navbat.push({ vaqt: this.hozir + ms, fn });
}
#keyingi(oxiri) {
this.navbat.sort((a, b) => a.vaqt - b.vaqt);
const t = this.navbat[0];
if (!t || t.vaqt > oxiri) return undefined;
return this.navbat.shift();
}
sur(ms) {
const oxiri = this.hozir + ms;
for (let t; (t = this.#keyingi(oxiri)); ) {
this.hozir = t.vaqt;
t.fn();
}
this.hozir = oxiri;
}
async surAsync(ms) {
const oxiri = this.hozir + ms;
const bosh = () => new Promise((r) => haqiqiyTimeout(r, 0));
await bosh(); // mikrovazifalar ishlab olsin
for (let t; (t = this.#keyingi(oxiri)); ) {
this.hozir = t.vaqt;
t.fn();
await bosh();
}
this.hozir = oxiri;
}
}
async function sina(usul) {
const soat = new SoxtaSoat();
const kut = (ms) => new Promise((ok) => soat.setTimeout(ok, ms));
let n = 0;
let tugadi = false;
const ish = () => (++n === 1
? Promise.reject(new Error("429"))
: Promise.resolve("ok"));
(async () => {
try { await ish(); } catch { await kut(5000); await ish(); }
tugadi = true;
})();
if (usul === "sur") soat.sur(5000);
else await soat.surAsync(5000);
await new Promise((r) => haqiqiyTimeout(r, 0));
console.log(usul, "→ tugadi:", tugadi,
"| navbatda:", soat.navbat.length);
}
await sina("sur");
await sina("surAsync");Konsolda:
sur → tugadi: false | navbatda: 1
surAsync → tugadi: true | navbatda: 0Model Vitest chiqishini aynan takrorlaydi: sinxron surishdan keyin taymer navbatda qoladi. Model soddalashtirilgan — haqiqiy soxta soat Date, setInterval, clearTimeout va boshqalarni ham almashtiradi. Lekin g'oya bir: await bor kodda vaqt ...Async bilan suriladi.
Diqqat: Sinxron surish xatosi har doim ham yiqilish bilan tugamaydi. Taymer keyinroq qo'yiladi va keyingi testda ishlab ketishi mumkin.
afterEachdagivi.useRealTimers()va har testning o'z soxta soati — shuning uchun majburiy.
Tekshirib ko'ring: Test
await vi.advanceTimersByTimeAsync(4_999)dan keyintugadi === falseni,advanceTimersByTimeAsync(1)dan keyintrueni tekshiradi. Nega ikkala tekshiruv kerak, faqat ikkinchisi yetmaydimi?
Javob
Faqat ikkinchisi "5 soniyadan keyin tugadi" ni isbotlaydi, lekin "5 soniyadan oldin tugamadi" ni emas. Kod Retry-After ni e'tiborsiz qoldirib, darhol qayta urinsa ham ikkinchi tekshiruv o'tadi. 4 999 ms dagi false aynan chegarani tekshiradi — Vitest II darsidagi debounce testida 399 va 400 ms ni tekshirgandek.
3.4 Soxta soat yoki soat parametri?
Vaqt va tasodifni boshqarish darsida ikkinchi yo'lni ham ko'rgansiz: soatni funksiyaga parametr qilib, tashqaridan berish. U darsda buni "ukol" deb atagandik; 30-qismdan beri kursdagi umumiy nomi — bog'liqlikni tashqaridan berish (DI). G'oya bir: kod vaqtni o'zi olmaydi, unga beriladi. Kanonda ikkalasi bor. ChidamliProvayder ning sozlamasida kut va soat maydonlari bor — "testlar uchun". Resilience naqshlari darsidagi chidamlilik.test.ts ularni soxta funksiyalar bilan to'ldiradi. Yangi asinxron.test.ts esa ataylab haqiqiy setTimeout ni qoldiradi va soxta soat bilan tekshiradi.
| Usul | Afzalligi | Kamchiligi |
|---|---|---|
Soat parametri (kut, soat) |
test qaysi soatni ishlatishini aniq ko'radi | prodakshndagi haqiqiy setTimeout yo'li tekshirilmaydi |
| Soxta soat | kod o'zgarmaydi; haqiqiy yo'l tekshiriladi | global holat; await tuzog'i |
Ikkalasini birga ishlatish — eng ishonchli: mantiq soat parametri bilan, haqiqiy taymer yo'li — soxta soat bilan.
4. Navbat va cron: ishlovchini to'g'ridan chaqirish
4.1 BullMQ ishchisi
BullMQ asoslari darsida ish (job) navbatga tushadi, ishchi (worker) uni oladi va ishlovchi funksiyani chaqiradi. Ishlovchi — oddiy funksiya: ish obyektini oladi, natija qaytaradi. Uni testlash uchun na Redis, na navbat kerak. Kanon navbat.test.ts dan parcha (qatorlar telefon uchun bo'lingan):
const ish = {
data: { vazifaId: v, foydalanuvchiId: f, muddat: yangiMuddat },
} as Job<EslatmaMalumoti>;
assert.equal(await eslatmaIshlovi(d)(ish), "eskirgan");Vaziyat: eslatma ishi rejalashtirilgandan keyin foydalanuvchi muddatni o'zgartirdi. Ish eski muddat bilan keldi — ishlovchi bildirishnoma yubormasligi va "eskirgan" qaytarishi kerak. Test ishni qo'lda yasaydi va ishlovchini sinxron chaqiradi (await bilan, lekin navbatsiz). Natija — millisekundlarda.
Bu yo'l ishlovchining mantiqini tekshiradi. Lekin "navbatga tushgan ish haqiqatan ishchiga yetadimi" — yo'q.
4.2 Butun zanjir: haqiqiy Redis bilan
Shu faylning birinchi testi butun yo'lni yuradi: API'da muddat qo'yiladi → outbox → tizim navbati → kechiktirilgan (delayed) eslatma ishi → bildirishnoma → email yetkazish → Mailpit. Muddat ataylab "30 daqiqa + 1.5 soniya" qilib qo'yiladi: eslatma 30 daqiqa oldin yuborilgani uchun ish ~1.5 soniyadan keyin ishlaydi. Test haqiqiy 1.5 soniyani kutadi — soxta soat bu yerda yordam bermaydi, chunki kechikishni Redis ichidagi BullMQ hisoblaydi, Node'ning setTimeout i emas.
Natijani esa test kutib tekshiradi:
const kutib = async <T>(
f: () => Promise<T | undefined>, ms = 8000,
): Promise<T> => {
const t0 = Date.now();
for (;;) {
const x = await f();
if (x !== undefined) return x;
if (Date.now() - t0 > ms) throw new Error("kutish tugadi");
await new Promise((ok) => setTimeout(ok, 50));
}
};kutib — frontenddagi waitFor ning backend nusxasi (Test strategiyasi va flaky testlar darsidagi «Async: "darhol" va "tugagach"»): shart bajarilguncha har 50 ms tekshiradi, 8 soniyadan keyin taslim bo'ladi. Muhim nuqta — ikkita raqam bor. 50 ms — qanchalik tez javob beramiz. 8 s — qancha sabr qilamiz. Shart bajarilishi bilan kutish tugaydi, shuning uchun katta chegara bo'sh mashinada testni sekinlashtirmaydi — u faqat yuklangan mashina uchun ehtiyot chegarasi.
Ishlatilishi:
await kutib(async () =>
(await navbatlar.eslatma.getDelayedCount()) === 1
? true : undefined);
const b = await kutib(async () => (await pool.query(
"SELECT id, matn FROM bildirishnomalar " +
"WHERE tur = 'vazifa.muddat'")).rows[0]);Avval navbatda bitta kechiktirilgan ish paydo bo'lishini, keyin bazada bildirishnoma qatorini kutadi. Har qadam o'z sharti bilan — "2 soniya uxlab, keyin tekshiramiz" emas.
4.3 Cron ishi
Kanonning ishchisi har kuni 04:15 da saqlash-muddati ishini bajaradi (Ma'lumot saqlash muddati darsi). Testda 04:15 ni kutmaymiz va soxta soat bilan jadvalni ham aylantirmaymiz. Jadval — ikki narsa: "qachon" (cron ifodasi) va "nima" (ish). "Nima" ni to'g'ridan chaqiramiz. saqlash.test.ts dan:
const n = await tizimIshlovi({ ishchi } as never)(
{ name: "saqlash-muddati" } as never,
) as Record<string, unknown>;Muhim detal — ishchi ulanishi haqiqiy vazifalar_ishchi roli bilan ochilgan. Test ishning mantig'idan tashqari huquqlarni ham tekshiradi: prodakshnda ish aynan shu rol bilan ishlaydi. Superuser bilan o'tgan test prodakshndagi permission denied ni ko'rsatmas edi.
"Qachon" ni esa alohida tekshirish mumkin: cron ifodasini tahlil qilib, keyingi ishga tushish vaqtini hisoblash. Kanonda bunday test yo'q — ifoda bitta qator va Cron va rejali vazifalar darsida o'qilgan.
Tekshirib ko'ring: Nega navbat testi eslatma ishini "30 daqiqa + 1.5 soniya" muddat bilan rejalashtiradi, 30 daqiqa bilan emas?
Javob
Eslatma muddatdan 30 daqiqa oldin yuboriladi. Muddat aynan 30 daqiqa bo'lsa, eslatma vaqti "hozir" — ish kechiktirilmay darhol bajariladi va test "kechiktirilgan navbatda bitta ish" holatini ko'ra olmaydi. 1.5 soniya qo'shimcha — ish avval kechiktirilgan ro'yxatga tushishi, keyin vaqti kelganda ishlashi uchun. Bu ham chegaraviy holat.
5. Webhook imzosi va vaqt
Chiquvchi webhook'larni (Chiquvchi webhook'larni yuborish) kanon Standard Webhooks formatida imzolaydi: webhook-id, webhook-timestamp va webhook-signature sarlavhalari. Qabul qiluvchi uchta narsani tekshiradi: imzo to'g'ri, tana o'zgarmagan, vaqt belgisi eski emas (sukut 300 s).
Bu yerda ham vaqt bor. Kanonning tekshir() funksiyasi hozirSoniya ni parametr sifatida qabul qiladi — vaqt tashqaridan beriladi. Testda soxta soat kerak emas. Shu g'oyani brauzerda Web Crypto bilan modellashtiramiz (formatni Kiruvchi webhook'larni qabul qilish darsidan bilasiz):
const enc = new TextEncoder();
async function imzola(sir, id, vaqt, tana) {
const kalit = await crypto.subtle.importKey(
"raw", enc.encode(sir),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
const xom = await crypto.subtle.sign(
"HMAC", kalit, enc.encode(`${id}.${vaqt}.${tana}`),
);
return "v1," + btoa(String.fromCharCode(...new Uint8Array(xom)));
}
async function tekshir(sir, s, tana, hozir) {
if (s.vaqt < hozir - 300) return "eski";
if (s.vaqt > hozir + 300) return "kelajak";
const kutilgan = await imzola(sir, s.id, s.vaqt, tana);
return kutilgan === s.imzo ? "ok" : "imzo";
}
const SIR = "test-sir";
const HOZIR = 1_793_000_000;
const tana = '{"tur":"vazifa.yaratildi","id":7}';
const s = {
id: "msg_1", vaqt: HOZIR - 10,
imzo: await imzola(SIR, "msg_1", HOZIR - 10, tana),
};
console.log("to'g'ri:", await tekshir(SIR, s, tana, HOZIR));
console.log("tana o'zgardi:",
await tekshir(SIR, s, tana.replace("7", "8"), HOZIR));
console.log("10 daqiqadan keyin:",
await tekshir(SIR, s, tana, HOZIR + 600));
console.log("boshqa sir:", await tekshir("boshqa", s, tana, HOZIR));Konsolda:
to'g'ri: ok
tana o'zgardi: imzo
10 daqiqadan keyin: eski
boshqa sir: imzoTo'rt holat — to'rt test. Uchinchisi soxta soatsiz: "hozir" ni parametr bilan 600 soniya oldinga qo'ydik. Model soddalashtirilgan: kanon imzolarni vaqtga chidamli usulda solishtiradi (timingSafeEqual, vaqt hujumi haqida) va bir sarlavhada bir nechta imzoni qabul qiladi. Kanonning webhook-api.test.ts testi esa haqiqiy yo'lni yuradi: obuna → vazifa hodisalari → ishchi yuboradi → test serveri qabul qiladi → har kelgan so'rov imzosi tekshir() bilan tekshiriladi.
6. Email va realtime: tashqi tizimni kutib tekshirish
6.1 Mailpit
29-qismda emaillarni lokal Mailpit serveriga yubordik (Email yuborish: SMTP va Nodemailer). Mailpit xatlarni ushlab qoladi va HTTP API orqali ko'rsatadi. Test SMTP'ni emas, natijani tekshiradi: "Azizga 'Muddat yaqin' mavzuli xat keldimi?":
const qidiruv = encodeURIComponent(
`to:${email} subject:"Muddat yaqin"`);
const xat = await kutib(async () => {
const r = await fetch(`${API}/api/v1/search?query=${qidiruv}`);
const j = await r.json() as { messages: { Subject: string }[] };
return j.messages[0];
});
assert.equal(xat.Subject, "Muddat yaqin");Bu parcha kanondagi bitta uzun qatorni o'qish uchun bo'lib yozilgan (ma'no o'zgarmagan). Yana kutib: xat navbat → ishchi → SMTP → Mailpit yo'lini bosib o'tadi, bu vaqt oladi. Qidiruv to: va subject: bilan — boshqa testlar yuborgan xatlar aralashmasin.
6.2 SSE va WebSocket
Kanonda realtime — SSE (Server-Sent Events, /v1/oqim). jonli.test.ts haqiqiy fetch oqimini o'qiydi, hodisalarni ro'yxatga yig'adi va yana kutib tekshiradi:
const h1 = await kutib(() => oyna.hodisalar[0]);Bu kutib boshqacharoq: sinxron funksiya oladi, har 15 ms tekshiradi, 3 s kutadi — hodisalar xotiradagi massivga tushadi, bazaga so'rov kerak emas.
WebSocket uchun g'oya o'sha: test mijoz sifatida ulanadi, hodisani Promise bilan kutadi va oxirida ulanishni yopadi. Socket.IO ishlatadigan loyihalarda bu socket.io-client bilan qilinadi. Kanonda Socket.IO yo'q (29-qism qarori — SSE), shuning uchun quyidagi parcha sinalmagan (hujjat bo'yicha, 2026-11):
import { io } from "socket.io-client";
const mijoz = io(`http://127.0.0.1:${port}`, {
transports: ["websocket"], forceNew: true,
});
const keldi = new Promise((ok) => mijoz.once("vazifa", ok));
await yaratish(); // API orqali vazifa qo'shamiz
assert.deepEqual(await keldi, { id: 7, tur: "yaratildi" });
mijoz.close();Ikki muhim qoida (har qanday realtime testi uchun): obunani hodisadan oldin oching (aks holda hodisa o'tib ketadi), va close() ni unutmang — ochiq ulanish test jarayonini to'xtatmaydi.
7. Kanon topgan hodisa: "javob keldi" ≠ "ish tugadi"
31-qism boshida kanonning birinchi to'liq tekshiruvida ikkita test yiqildi. Biri — Telegram bot testi. Bot /yangi buyrug'iga "matnini yozing" deb javob beradi, test esa shu zahoti Redis'da dialog holati bormi deb tekshirardi:
n = soni(701); tg.yozadi(701, "/yangi");
assert.match((await javob(701, n)).text ?? "", /matnini yozing/);
assert.ok((await redis.keys("vz:tg:dialog:*")).length > 0);Odatda o'tardi. Yuk ostida — yiqildi. Sabab: grammY'ning dialoglar (conversations) plagini holatni ishlovchi tugagach yozadi, javob xabari esa undan oldin jo'naydi. Test "javob keldi" ni "holat yozildi" deb hisoblagan. Bo'sh mashinada oraliq mikrosoniyalar, yuklanganida — test ulgurmaydi.
Tuzatish — taxminiy pauza emas, kutib:
await kutib(async () =>
((await redis.keys("vz:tg:dialog:*")).length > 0
? true : undefined));Bu xato turi juda keng tarqalgan. Har asinxron tizimda "foydalanuvchiga javob" va "ichki holat" alohida vaqtda yoziladi. Vitest'ning o'zida ham shu ishni qiladigan tayyor vositalar bor — vi.waitFor va expect.poll. Kanon o'z kutib funksiyasini yozgan, lekin ularni ham sinab ko'rdik:
// "Ishchi" holatni 120 ms dan keyin yozadi
function ishchiYasa() {
const holat = new Map<string, string>();
setTimeout(() => holat.set("dialog:701", "muddat"), 120);
return holat;
}
test("darhol tekshirish", () => {
const holat = ishchiYasa();
expect(holat.get("dialog:701")).toBe("muddat");
});
test("vi.waitFor", async () => {
const holat = ishchiYasa();
const q = await vi.waitFor(() => {
const v = holat.get("dialog:701");
if (v === undefined) throw new Error("hali yo'q");
return v;
}, { timeout: 1000, interval: 20 });
expect(q).toBe("muddat");
});
test("expect.poll", async () => {
const holat = ishchiYasa();
await expect.poll(() => holat.get("dialog:701"), { timeout: 1000 })
.toBe("muddat");
});Biz ishga tushirdik (Vitest 5.0.5; millisekundlar har yugurishda boshqacha):
× |asosiy| test/demo31b.test.ts > darhol tekshirish 9ms
→ expected undefined to be 'muddat' // Object.is equality
✓ |asosiy| test/demo31b.test.ts > vi.waitFor 130ms
✓ |asosiy| test/demo31b.test.ts > expect.poll 178msNima bo'ldi? Birinchi test "ishchi" holatni yozmasidan oldin tekshirdi — undefined oldi va yiqildi. Qolgan ikkitasi kutdi va ~130–180 ms da o'tdi: shart bajarilishi bilan kutish tugadi, to'liq 1 soniya emas.
vi.waitFor funksiya xato tashlamay qolguncha qayta chaqiradi, expect.poll — tekshiruv o'tguncha. Vitest hujjatiga ko'ra (2026-11) vi.waitFor sukut bo'yicha har 50 ms da tekshiradi va 1 000 ms da taslim bo'ladi — kanondagi kutib bilan bir xil oraliq. Uchala vosita (kutib, vi.waitFor, expect.poll) bir g'oyaning shakllari.
8. Ko'p uchraydigan xatolar
| Xato | Nima bo'ladi | To'g'risi |
|---|---|---|
await bor kodda advanceTimersByTime |
taymer keyin qo'yiladi, test yiqiladi yoki keyingi testga "oqadi" | advanceTimersByTimeAsync |
vi.useRealTimers() afterEach da yo'q |
soxta soat keyingi testlarga o'tadi | beforeEach / afterEach juftligi |
| "200 ms uxlab tekshiramiz" | bo'sh mashinada o'tadi, CI'da yiqiladi | shart bilan kutish: kutib, vi.waitFor |
| Soxta soat + haqiqiy Redis/Postgres | drayver taymerlari to'xtaydi, test osiladi | soxta soat — faqat toza mantiq uchun |
| Ishni faqat navbat orqali testlash | sekin va sabab topish qiyin | mantiq — ishlovchini chaqirib; zanjir — bitta integratsiya testi |
| Ishni superuser bilan testlash | prodakshndagi permission denied ko'rinmaydi |
ishchining haqiqiy roli |
| Realtime'da obunani hodisadan keyin ochish | hodisa o'tib ketadi, test osiladi | avval obuna, keyin harakat |
Uchinchi qatordagi "uxlab tekshirish" ning qisqa tarixi: 22-qismda frontend testida, 31-qismda Telegram bot testida — ikkala joyda ham beqaror test aynan shundan tug'ildi.
9. Mashqlar
Mashqlar kurs/mashqlar/31/31-asinxron/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): bo'shliqlarni to'ldiring
awaitbor kodda soxta vaqtni surish uchunvi.advanceTimersByTimeishlatiladi.- Soxta soat har testdan keyin
vi.use()bilan o'chiriladi. - Navbat testida kechiktirilgan ishlar soni
getDelayed()bilan olinadi. - Kanondagi
kutibsukut bo'yicha har ms da tekshiradi. - Webhook vaqt belgisi sukut bo'yicha soniyadan eski bo'lsa rad etiladi.
Yechim
- Async — mikrovazifalarga navbat beradi.
- RealTimers —
afterEachichida. - Count —
navbatlar.eslatma.getDelayedCount(). - 50 — chegara esa 8 000 ms.
- 300 — besh daqiqa; undan keyin
"eski".
2-mashq (o'rta): kesh muddati soxta soat bilan
KeshTTL klassi berilgan: yoz(kalit, qiymat, ms) va ol(kalit); vaqtni hozir() funksiyasidan oladi. Soxta soat yozing (oddiy o'zgaruvchi) va tekshiring: 999 ms da qiymat bor, 1 000 ms da ham bor (chegara — "keyin" emas), 1 001 ms da yo'q. Ishora: kanondagi XotiraKesh ham tugash < Date.now() deb solishtiradi.
Yechim
class KeshTTL {
#m = new Map();
constructor(hozir) { this.hozir = hozir; }
yoz(k, q, ms) { this.#m.set(k, { q, tugash: this.hozir() + ms }); }
ol(k) {
const x = this.#m.get(k);
if (!x || x.tugash < this.hozir()) {
this.#m.delete(k);
return undefined;
}
return x.q;
}
}
let vaqt = 0;
const kesh = new KeshTTL(() => vaqt);
kesh.yoz("bayroqlar", "ai-qadamlar:100", 1000);
for (const t of [999, 1000, 1001]) {
vaqt = t;
console.log(`${t} ms →`, kesh.ol("bayroqlar"));
}Konsolda:
999 ms → ai-qadamlar:100
1000 ms → ai-qadamlar:100
1001 ms → undefined1 000 ms da qiymat hali bor: tugash < hozir qat'iy solishtirish. Agar <= yozilsa, 1 000 ms da yo'qolardi — off-by-one aynan shu chegaraviy testda ushlanadi. Bu yechimda soxta soat — soat parametri: hozir funksiyasi tashqaridan berildi.
3-mashq (qiyin): o'z kutib funksiyangiz
kutib(f, { chegara, oraliq }) yozing: f() undefined dan boshqa qiymat qaytarguncha har oraliq ms da chaqiradi, chegara dan oshsa Error("kutish tugadi (100 ms)") shaklidagi xato tashlaydi. Ikki holatni sinang: holat 120 ms da paydo bo'ladi (chegara 1 000 ms) va hech qachon paydo bo'lmaydi (chegara 100 ms). Chiqishda o'lchangan vaqtni emas, natija va xato matnini ko'rsating. Ishora: Date.now(), await new Promise((ok) => setTimeout(ok, ms)).
Yechim
async function kutib(f, { chegara = 1000, oraliq = 20 } = {}) {
const t0 = Date.now();
for (;;) {
const x = await f();
if (x !== undefined) return x;
if (Date.now() - t0 > chegara) {
throw new Error(`kutish tugadi (${chegara} ms)`);
}
await new Promise((ok) => setTimeout(ok, oraliq));
}
}
const holat = new Map();
setTimeout(() => holat.set("dialog:701", "muddat"), 120);
console.log(await kutib(() => holat.get("dialog:701")));
try {
await kutib(() => holat.get("dialog:999"), { chegara: 100 });
} catch (e) {
console.log(e.message);
}Konsolda:
muddat
kutish tugadi (100 ms)Xato matniga urinishlar sonini yoki o'tgan vaqtni qo'shmadik: ular mashina tezligiga bog'liq (100 ms ichida 20 ms li oraliq bilan taxminan 5–6 urinish) va chiqish har safar boshqacha bo'lardi. Xato matnida esa chegaraning o'zi — u o'zgarmaydi va xatoni o'qigan odamga "qancha sabr qilingan" ni aytadi. Kanondagi kutib dan farqi — sozlamalar obyekti va xato matni.
10. Loyiha qadami: test/asinxron.test.ts
Loyihaning yakuniy bosqichi to'rt darsga bo'lingan. Bu darsda — vaqtga bog'liq kod uchun yangi test fayli. Navbat, SSE, Mailpit, cron va Telegram dialogi uchun testlar allaqachon bor (navbat, jonli, bildirishnoma, saqlash, telegram-bot) — ular 29–31-qismlarda yozilgan va yuqoridagi uch yo'lni ishlatadi. Yangi fayl qolgan bo'shliqni yopadi: jarayon ichidagi kesh va qayta urinish vaqtlari.
test/asinxron.test.ts (kanon fayli; qatorlar telefon uchun bo'lingan, ma'no aynan):
// asinxron.test.ts — vaqtga bog'liq kodni SOXTA SOAT bilan
// testlash (kutmasdan): bayroqlar keshi (30 s TTL), AI javob keshi
// TTL, retry backoff'i haqiqiy setTimeout bilan —
// vi.useFakeTimers + advanceTimersByTimeAsync. Navbat (BullMQ), SSE,
// Mailpit, cron (ishchi) — mavjud integratsiya testlarida.
import { afterEach, beforeEach, test, vi } from "vitest";
import assert from "node:assert/strict";
import type { Pool } from "pg";
import { BayroqlarXizmati } from "../src/bayroq/bayroqlar.ts";
import { XotiraKesh } from "../src/ai/nazorat.ts";
import { ChidamliProvayder } from "../src/ai/chidamli.ts";
import { SoxtaProvayder } from "../src/ai/soxta.ts";
import { LlmXatosi } from "../src/ai/llm.ts";
beforeEach(() => {
vi.useFakeTimers({ now: new Date("2026-11-03T09:00:00Z") });
});
afterEach(() => {
vi.useRealTimers();
});
test("bayroqlar keshi: 30 s ichida bazaga qayta bormaydi, " +
"30 s dan keyin — boradi; tozala() — darhol", async () => {
let sorovlar = 0;
const qator = {
kalit: "ai-qadamlar", yoqilgan: true, foiz: 100,
rollar: [], foydalanuvchilar: [],
};
const pool = {
query: () => {
sorovlar++;
return Promise.resolve({ rows: [qator] });
},
} as unknown as Pool;
const b = new BayroqlarXizmati(pool);
await b.yoqilganmi("ai-qadamlar", { id: 1 });
vi.advanceTimersByTime(29_999);
await b.yoqilganmi("ai-qadamlar", { id: 2 });
assert.equal(sorovlar, 1);
vi.advanceTimersByTime(1);
await b.yoqilganmi("ai-qadamlar", { id: 3 });
assert.equal(sorovlar, 2);
b.tozala();
await b.hammasi();
assert.equal(sorovlar, 3);
});
test("AI javob keshi (xotira): TTL tugagach yo'q; " +
"chegara oshsa eng eskisi chiqadi", async () => {
const k = new XotiraKesh(2);
await k.yoz("a", "1", 60);
vi.advanceTimersByTime(59_000);
assert.equal(await k.ol("a"), "1");
vi.advanceTimersByTime(1_001);
assert.equal(await k.ol("a"), undefined);
await k.yoz("b", "2", 60);
await k.yoz("c", "3", 60);
await k.yoz("d", "4", 60);
assert.deepEqual(
[await k.ol("b"), await k.ol("c"), await k.ol("d")],
[undefined, "3", "4"],
);
});
test("retry backoff haqiqiy setTimeout bilan — soxta soat oldinga " +
"suriladi (test 0 ms kutadi); Retry-After 5 s hurmat qilinadi",
async () => {
const p = new SoxtaProvayder({
javob: () => "ok",
xatolar: [new LlmXatosi("limit", "429", 5000, 429)],
});
const c = new ChidamliProvayder(p, { qaytaUrinish: 1 });
let tugadi = false;
const v = c
.yarat({ tizim: "t", xabarlar: [{ rol: "user", matn: "x" }] })
.then((j) => {
tugadi = true;
return j;
});
await vi.advanceTimersByTimeAsync(4_999);
assert.equal(tugadi, false);
await vi.advanceTimersByTimeAsync(1);
assert.equal((await v).matn, "ok");
assert.equal(p.chaqiruvlar.length, 2);
});Uchta testni o'qish yo'riqnomasi:
- Bayroqlar keshi.
pool— soxta obyekt: faqatquerybor va u har chaqiruvni sanaydi. Feature flag'lar darsidagiBayroqlarXizmatibayroqlarni 30 s keshlaydi. 29 999 ms da — hali bitta so'rov, 30 000 ms da — ikkinchisi.tozala()keshni darhol bo'shatadi (admin bayroqni o'zgartirganda). Bu yerdaawaitbor-u, lekin sinxronadvanceTimersByTimeyetadi: kesh taymer qo'ymaydi, faqatDate.now()ga qaraydi. Soxta soatDateni ham boshqaradi. - AI keshi.
XotiraKesh(2)— ikki yozuvli kesh. 60 s TTL: 59 s da bor, 60.001 s da yo'q. Keyin uchta yozuv — sig'im 2, eng eskisi (b) chiqib ketadi. E'tibor bering: bu LRU emas, birinchi yozilgan birinchi chiqadi. - Retry. Soxta provayder birinchi chaqiruvda
429vaRetry-After: 5000 msli xato tashlaydi.ChidamliProvayderhaqiqiysetTimeoutbilan kutadi. Bu yerda...Asyncshart — «Soxta soat:awaittuzog'i» bo'limidagi sababga ko'ra.
useFakeTimers({ now }) soxta soatni aniq sanadan boshlaydi — test kun, oy va vaqt zonasiga bog'liq bo'lmasin.
Ishga tushiring:
npx vitest run test/asinxron.test.ts --reporter=verboseBiz ishga tushirdik (bazasiz ham ishlaydi — uchala test soxta obyektlar bilan; qatorlar qisqartirilgan):
✓ |asosiy| test/asinxron.test.ts > bayroqlar keshi: 30 s ichida bazaga qayta bormaydi, 30 s dan keyin — boradi; tozala() — darhol 6ms
✓ |asosiy| test/asinxron.test.ts > AI javob keshi (xotira): TTL tugagach yo'q; chegara oshsa eng eskisi chiqadi 2ms
✓ |asosiy| test/asinxron.test.ts > retry backoff haqiqiy setTimeout bilan — soxta soat oldinga suriladi (test 0 ms kutadi); Retry-After 5 s hurmat qilinadi 2ms
Test Files 1 passed (1)
Tests 3 passed (3)Uchala test birgalikda ~10 ms. Haqiqiy vaqt bilan ular kamida 30 + 60 + 5 = 95 soniya kutardi.
Hali commit qilmang — loyihaning bu bosqichi keyingi uch darsda davom etadi: shartnoma testlari, yuklama o'lchovi va CI. Commit — Test arxitekturasi va CI darsida.
11. Real ishda
- Qayerda uchraydi: to'lov holati webhook bilan keyinroq keladi, email ishchi orqali jo'naydi, hisobot kechasi tayyorlanadi — bularning har birida "keyinroq" bor. Jamoalardagi beqaror testlarning katta qismi aynan shu joylardan chiqadi.
- Vositalar: Vitest/Jest soxta taymerlari,
vi.waitFor,expect.poll; Testcontainers'da haqiqiy Redis; Mailpit (ilgari MailHog) — email uchun; BullMQ'da test uchun alohidaprefix(kanondat45-<pid>) — parallel testlar bir-birining navbatiga tegmasin. - Intervyu savollari: "Navbat ishchisini qanday testlaysiz?", "Soxta taymerlar
asynckodda nega ishlamay qoladi?", "Beqaror testni qanday tuzatgansiz?", "Cron ishini qanday testlaysiz — 04:15 ni kutasizmi?"
Xulosa
- Asinxron kodni testlashning uch yo'li: ishlovchini to'g'ridan chaqirish (mantiq), soxta soat (vaqt chegaralari), kutib tekshirish (haqiqiy zanjir).
awaitbor kodda soxta vaqtadvanceTimersByTimeAsyncbilan suriladi — sinxron surishda taymer hali qo'yilmagan bo'ladi.- Cron ishi jadvalni kutmasdan, nomi bilan chaqiriladi — va haqiqiy rol bilan, huquqlar ham tekshirilsin.
- Haqiqiy Redis/Mailpit/SSE bilan — "N ms uxlash" emas, shart bilan kutish (
kutib,vi.waitFor,expect.poll), ikki raqam: oraliq va chegara. - "Javob keldi" ≠ "ish tugadi": kanonning Telegram testi shu sabab yuk ostida yiqilgan.
test/asinxron.test.ts: 30 s kesh, 60 s TTL va 5 s retry — ~10 ms da.
Keyingi dars: Contract va API sxema testlari — API javobi frontend kutgan shakldan chiqib ketsa, buni CI'da qanday ushlaymiz: OpenAPI surati va iste'molchi shartnomasi.
Manbalar
- Vitest hujjati — Fake Timers (
advanceTimersByTimeAsync): https://vitest.dev/api/vi.html#vi-advancetimersbytimeasync (sinalmagan URL, 2026-11) - Vitest hujjati —
vi.waitFor: https://vitest.dev/api/vi.html#vi-waitfor (sinalmagan URL, 2026-11) - Vitest hujjati —
expect.poll: https://vitest.dev/api/expect.html#poll (sinalmagan URL, 2026-11) - BullMQ hujjati — Delayed jobs: https://docs.bullmq.io/guide/jobs/delayed (sinalmagan URL, 2026-11)
- Mailpit hujjati — API v1, search: https://mailpit.axllent.org/docs/api-v1/ (sinalmagan URL, 2026-11)
- Standard Webhooks spetsifikatsiyasi: https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md (sinalmagan URL, 2026-11)
- Socket.IO hujjati — Testing: https://socket.io/docs/v4/testing/ (sinalmagan, 2026-11)
- Kanon (IlmHamroh, 2026-11-03):
test/asinxron.test.ts,navbat.test.ts,saqlash.test.ts,jonli.test.ts,telegram-bot.test.tsva uning beqarorlik tuzatishi. Soxta soat namoyishi,vi.waitFor/expect.pollvaasinxron.test.tsyugurishi — biz ishga tushirdik (2026-11-03, Vitest 5.0.5, Node 24.21.0); brauzer modellari va mashqlar — Node 24.21.0.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!