Mundarija (40)
- Bu darsda
- 1. Nega bu kerak?
- 2. vi.fn(): chaqiruvlarni yozadigan funksiya
- 2.1 Kim, necha marta, nima bilan?
- 2.2 Xulq berish: nima qaytarsin?
- 3. vi.spyOn va vi.stubGlobal: mavjud narsani almashtirish
- 3.1 vi.spyOn — metodni kuzatish
- 3.2 vi.stubGlobal — global qiymat
- 4. vi.mock: butun modulni almashtirish
- 4.1 Muammo: import qilingan funksiya
- 4.2 Qisman soxta: importOriginal
- 4.3 Ko'tarilish (hoisting)
- 4.4 Natija oynasida: bog'liqlikni tashqaridan berish
- 5. Soxta taymerlar: vaqtni test boshqaradi
- 5.1 Debounce'ni bir zumda sinash
- 5.2 "Hozir" ni o'rnatish: ish vaqti
- 5.3 Kanon topgan ikki tuzoq
- 6. Har test toza boshlansin: beforeEach / afterEach
- 6.1 Tartib
- 6.2 Tozalashning uch darajasi
- 7. test.each: bitta test — ko'p ma'lumot
- 8. Snapshot: natija nusxasini saqlash
- 9. Coverage: qaysi kod ishlamadi?
- 9.1 O'rnatish va o'qish
- 9.2 Chegara (thresholds)
- 10. Vazifalar qadami: coverage va chegara (holat 03)
- 10.1 O'rnatish
- 10.2 vitest.config.ts
- 10.3 Raqamlar
- 10.4 Ikki yangi test fayli
- 10.5 Natija va commit
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): eslatma bir marta ketsin
- 3-mashq (qiyin): kesh va josus
- 4-mashq: Vazifalar qadami
- 13. Real ishda
- Xulosa
- Manbalar
Vitest mock va spy: vi.fn, vi.spyOn, vi.mock, soxta taymerlar va coverage
Qisqacha: Test tashqi dunyoga (SMS, to'lov,
fetch, soat) tegmasligi kerak. Vitest'da buning uchun soxta (mock) vositalar bor. Masalan,vi.fn()— chaqiruvlarni yozadigan soxta funksiya,vi.spyOn(obyekt, "metod")— mavjud metodni kuzatish.vi.mock("./modul")butun modulni almashtiradi (fayl boshiga ko'tariladi),vi.stubGlobal— global qiymatni.vi.useFakeTimers()vaqtni test boshqaradigan qiladi: 15 daqiqavi.advanceTimersByTime()bilan bir zumda o'tadi. Har testdan keyin hammasiafterEachda tiklanadi. Coverage (qamrov) esa testlar kodning qaysi qatorini umuman ishga tushirmaganini ko'rsatadi.
Bu darsda
vi.fn()bilan soxta funksiya yasaysiz, chaqiruvlarini tekshirasiz va xulqini berasiz.vi.spyOnvavi.stubGlobalbilan mavjud metod va global qiymatni vaqtincha almashtirasiz.vi.mockqanday ko'tarilishini (hoisting) va qachon kerakligini tushuntirasiz.- Debounce va ish vaqtini soxta taymerlar bilan sinaysiz.
test.each, snapshot va coverage'ni ishlatasiz.
Oldin bilishingiz kerak: Vitest I, Birinchi avtomatik test: node:test (mock.fn), Hodisalarni boshqarish (vi.fn bilan birinchi tanishuv), Debounce va throttle, Taymerlar.
1. Nega bu kerak?
Oldingi darsda hamma testlar bitta vositaga — Vitest'ga — ko'chdi. Endi qiyinroq savol. «Bahor» oshxonasida buyurtma tayyor bo'lsa, mijozga SMS ketadi. Bu kodni qanday sinaymiz?
Haqiqiy SMS bilan sinasak — uchta muammo. Har npm test da Jasur akaning hisobidan pul yechiladi. Internet bo'lmasa, test yiqiladi, kod esa to'g'ri. Uchinchisi — eslatma "15 daqiqadan keyin" ketsa, test 15 daqiqa kutadi.
Yechim — tashqi narsani test vaqtida o'rinbosar bilan almashtirish. Xuddi kino suratga olishdagi kaskadyor kabi: xavfli sahnada aktyor o'rniga u chiqadi, tomoshabin farqni sezmaydi. Test uchun bunday o'rinbosarlarning umumiy nomi — test dublyori (test double).
Rasmda — kod tegadigan tashqi narsalar va har biriga mos Vitest vositasi.
flowchart LR
K["markReady()"] --> S["sendSms — tarmoq"]
K --> F["fetch — server"]
K --> T["setTimeout, Date.now — soat"]
S -.-> A["vi.fn / vi.mock"]
F -.-> B["vi.stubGlobal"]
T -.-> C["vi.useFakeTimers"]node:test darsida mock.fn() bilan shu g'oyani ko'rgansiz (mock.fn() → vi.fn(), mock.method() → vi.spyOn(), mock.timers → vi.useFakeTimers()). Vitest'da vositalar ko'proq.
2. vi.fn(): chaqiruvlarni yozadigan funksiya
2.1 Kim, necha marta, nima bilan?
vi.fn() — soxta funksiya (mock function). U hech narsa qilmaydi (undefined qaytaradi), lekin har chaqiruvni eslab qoladi. 17-qismda uni "josus" deb atagan edik (Hodisalarni boshqarish). Bugun ichini ochamiz.
Funksiyamiz SMS'ni o'zi yubormaydi — send ni parametr sifatida oladi. Testda unga vi.fn() beramiz:
import { describe, expect, test, vi } from "vitest";
function notifyReady(orders, send) {
for (const order of orders) {
if (order.status === "tayyor") {
send(order.phone, `Buyurtma #${order.id} tayyor`);
}
}
}
const orders = [
{ id: 1, phone: "+998 90 000 00 01", status: "tayyor" },
{ id: 2, phone: "+998 90 000 00 02", status: "pishmoqda" },
{ id: 3, phone: "+998 90 000 00 03", status: "tayyor" },
];
describe("notifyReady", () => {
test("faqat tayyor buyurtmalarga SMS", () => {
const send = vi.fn();
notifyReady(orders, send);
console.log(send.mock.calls.length, send.mock.calls[1]);
expect(send).toHaveBeenCalledTimes(2);
expect(send).toHaveBeenCalledWith(
"+998 90 000 00 03",
"Buyurtma #3 tayyor",
);
});
test("ataylab xato: uchta kutamiz", () => {
const send = vi.fn();
notifyReady(orders, send);
expect(send).toHaveBeenCalledTimes(3);
});
});Konsolda:
2 (2) ['+998 90 000 00 03', 'Buyurtma #3 tayyor']
✓ notifyReady > faqat tayyor buyurtmalarga SMS
× notifyReady > ataylab xato: uchta kutamiz
→ expected "vi.fn()" to be called 3 times, but got 2 times
Testlar: 1 yiqildi, 1 o'tdi (jami 2)send.mock.calls — har chaqiruv argumentlari massivi: calls[1] — ikkinchi chaqiruv (birinchisi calls[0]). Xato xabarining tarjimasi: "vi.fn() 3 marta chaqirilishi kutilgan edi, 2 marta chaqirildi". "vi.fn()" — soxtaning nomi; .mockName("send") bilan o'z nomingizni bersangiz, xabarda shu chiqadi.
Chaqiruvlarni tekshiradigan asosiy matcherlar:
| Matcher | Nimani tekshiradi |
|---|---|
toHaveBeenCalled() |
kamida bir marta chaqirilgan |
toHaveBeenCalledTimes(n) |
aynan n marta |
toHaveBeenCalledWith(a, b) |
biror chaqiruv aynan shu argumentlar bilan |
toHaveBeenLastCalledWith(…) |
oxirgi chaqiruv |
toHaveBeenNthCalledWith(2, …) |
ikkinchi chaqiruv |
Qisqaroq ikkitasi ham bor: toHaveBeenCalledOnce() va toHaveBeenCalledExactlyOnceWith(…) ("bir marta va aynan shu bilan").
Tekshirib ko'ring:
notifyReadyga bo'sh massiv bersak, qaysi matcher to'g'ri:expect(send).not.toHaveBeenCalled()yokiexpect(send).toHaveBeenCalledTimes(0)?
Javob
Ikkalasi ham to'g'ri va bir xil narsani tekshiradi. not.toHaveBeenCalled() o'qilishi tabiiyroq: "chaqirilmagan". Xabari ham aniq: necha marta chaqirilganini aytadi.
2.2 Xulq berish: nima qaytarsin?
Bo'sh vi.fn() doim undefined qaytaradi. Ko'pincha kod javobga qaraydi: to'lov tizimi { id, ok } qaytaradi, ba'zan xato tashlaydi. Soxtaga xulqni shunday beramiz:
| Metod | Ma'nosi |
|---|---|
mockReturnValue(v) |
doim v qaytaradi |
mockReturnValueOnce(v) |
faqat keyingi chaqiruvda |
mockResolvedValue(v) |
Promise.resolve(v) qaytaradi |
mockRejectedValue(e) |
Promise.reject(e) qaytaradi |
mockImplementation(fn) |
o'rniga fn ishlaydi |
Once qo'shimchali variantlar navbatga turadi: birinchi chaqiruvda birinchisi, keyin ikkinchisi. Bu qayta urinish (retry) mantiqini sinashda juda qulay:
import { expect, test, vi } from "vitest";
async function payWithRetry(total, pay) {
try {
return await pay(total);
} catch {
return await pay(total); // bitta qayta urinish
}
}
test("muvaffaqiyat — mockResolvedValue", async () => {
const pay = vi.fn().mockResolvedValue({ id: "T-17", ok: true });
const result = await payWithRetry(70000, pay);
expect(result).toEqual({ id: "T-17", ok: true });
expect(pay).toHaveBeenCalledWith(70000);
});
test("birinchi urinish yiqiladi, ikkinchisi o'tadi", async () => {
const pay = vi
.fn()
.mockRejectedValueOnce(new Error("Tarmoq uzildi"))
.mockResolvedValueOnce({ id: "T-18", ok: true });
const result = await payWithRetry(70000, pay);
expect(result.id).toBe("T-18");
expect(pay).toHaveBeenCalledTimes(2);
});
test("ikkalasi ham yiqilsa — xato tashqariga chiqadi", async () => {
const pay = vi
.fn()
.mockRejectedValue(new Error("Karta bloklangan"));
await expect(payWithRetry(70000, pay)).rejects.toThrow(
"Karta bloklangan",
);
console.log(pay.mock.results.map((r) => r.type));
});Konsolda:
✓ muvaffaqiyat — mockResolvedValue
✓ birinchi urinish yiqiladi, ikkinchisi o'tadi
(2) ['return', 'return']
✓ ikkalasi ham yiqilsa — xato tashqariga chiqadi
Testlar: 3 o'tdi (jami 3)mock.results — har chaqiruv natijasi. Ikkalasi "return", chunki soxta xato tashlamadi — rad etilgan Promise qaytardi. Sinxron throw bo'lganda "throw" yozilardi.
rejects — Promise rad etilishini kutadi (Vitest I dagi resolves ning juftligi). rejects.toThrow("…") — rad etish sababi shu matnli xato ekanini tekshiradi. await ni unutmang: usiz Vitest testni yiqitadi (Promise returned by … was not awaited).
3. vi.spyOn va vi.stubGlobal: mavjud narsani almashtirish
3.1 vi.spyOn — metodni kuzatish
vi.fn() yangi funksiya yasaydi. Lekin ba'zan kod tayyor obyektning metodini chaqiradi — uni parametr bilan berib bo'lmaydi. vi.spyOn(obyekt, "metod") — josus (spy): metodni o'rab oladi, chaqiruvlarni yozadi va asl metodni ishlataveradi. Xohlasangiz, xulqni almashtirasiz:
import { afterEach, expect, test, vi } from "vitest";
const sms = {
send(phone, text) {
console.log(`[haqiqiy SMS] ${phone}: ${text}`);
return true;
},
};
function remind(order) {
return sms.send(order.phone, `#${order.id} 10 daqiqada tayyor`);
}
const order = { id: 5, phone: "+998 90 000 00 05" };
afterEach(() => {
vi.restoreAllMocks(); // har testdan keyin asl metod qaytadi
});
test("spyOn — kuzatadi, asl metod ishlayveradi", () => {
const spy = vi.spyOn(sms, "send");
remind(order);
expect(spy).toHaveBeenCalledTimes(1);
});
test("mockImplementation — asl metod chaqirilmaydi", () => {
vi.spyOn(sms, "send").mockImplementation(() => false);
expect(remind(order)).toBe(false);
});
test("restoreAllMocks'dan keyin — yana asl", () => {
console.log(vi.isMockFunction(sms.send));
});Konsolda:
[haqiqiy SMS] +998 90 000 00 05: #5 10 daqiqada tayyor
✓ spyOn — kuzatadi, asl metod ishlayveradi
✓ mockImplementation — asl metod chaqirilmaydi
false
✓ restoreAllMocks'dan keyin — yana asl
Testlar: 3 o'tdi (jami 3)[haqiqiy SMS] qatori faqat bir marta chiqdi — birinchi testda. Ikkinchi testda asl metod almashgan. Uchinchida false — afterEach dagi restoreAllMocks() asl send ni joyiga qo'ydi.
restoreAllMocks ni unutish — eng ko'p uchraydigan xato. Usiz ikkinchi testdagi () => false keyingi testlarga ham "oqib" o'tadi. Testlar alohida o'tadi, birga yiqiladi — va sababini topish qiyin.
Bu usulni avval ko'rgansiz: useEffectEvent darsida vi.spyOn(window, "addEventListener") tinglovchilar sonini sanagan edi. Kanonning qoralama testi esa vi.spyOn(Storage.prototype, "setItem") bilan localStorage yozuvlarini kuzatadi («Vazifalar qadami» da).
3.2 vi.stubGlobal — global qiymat
fetch, navigator, location — globallar. Ularni vi.stubGlobal("nom", qiymat) almashtiradi, vi.unstubAllGlobals() esa asliga qaytaradi:
import { afterEach, expect, test, vi } from "vitest";
async function loadMenu() {
const res = await fetch("/api/menyu");
if (!res.ok) throw new Error(`Menyu yuklanmadi: ${res.status}`);
return res.json();
}
afterEach(() => {
vi.unstubAllGlobals(); // asl fetch qaytadi
});
test("server javob berdi", async () => {
const menu = [{ name: "Osh", price: 35000 }];
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: true, json: async () => menu }),
);
expect(await loadMenu()).toEqual(menu);
expect(fetch).toHaveBeenCalledWith("/api/menyu");
});
test("server 500 qaytardi", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: false, status: 500 }),
);
await expect(loadMenu()).rejects.toThrow("Menyu yuklanmadi: 500");
});Konsolda:
✓ server javob berdi
✓ server 500 qaytardi
Testlar: 2 o'tdi (jami 2)Tuzoq — kech qolgan stub. useEffect III darsida kanon shunga uchragan edi: server-sinxron.ts api klientini modul yuklanganda yaratadi va fetch ni o'shanda oladi. Testdagi vi.stubGlobal("fetch", …) esa keyin ishlaydi — klient eski fetch ni ushlab qolgan. Qoida: stub kod globalni o'qishidan oldin turishi kerak. Kanon shuning uchun soxta fetch ni sozlash faylida (vitest-sozlash.ts), test faylidan oldin qo'yadi.
Tekshirib ko'ring:
const narx = Date.now;deb modul boshida yozilgan bo'lsa, keyinDate.nowni almashtirish bu modulga ta'sir qiladimi?
Javob
Yo'q. narx modul yuklanganda asl funksiyaga havolani olib bo'lgan. Keyingi almashtirish Date.now xususiyatini o'zgartiradi, narx esa eskisiga qarab turadi. Xuddi "kech qolgan stub" tuzog'i.
4. vi.mock: butun modulni almashtirish
4.1 Muammo: import qilingan funksiya
Haqiqiy loyihada markReady sendSms ni parametr bilan olmaydi — import qiladi:
// order.js — buyurtma tayyor bo'lsa mijozga SMS
import { formatPhone, sendSms } from "./sms.js";
export async function markReady(order) {
const phone = formatPhone(order.phone);
await sendSms(phone, `Buyurtma #${order.id} tayyor`);
return { ...order, status: "tayyor" };
}Testda sendSms ni qanday almashtiramiz? vi.mock("./sms.js") — Vitest'ga "bu modul o'rniga soxtasini ber" deydi. Fabrikasiz chaqirilsa — avtomatik soxta (automock): moduldagi har funksiya vi.fn() ga aylanadi.
Natija oynasida modul fayllari yo'q, shuning uchun bu kod — o'z loyihangizda. Quyidagi chiqishlar haqiqiy Vitest 5.0.4 terminalidan (Node 24.21.0; yo'l qisqartirilgan, vaqt sizda boshqacha):
import { expect, test, vi } from "vitest";
import { markReady } from "../src/order.js";
import { sendSms } from "../src/sms.js";
vi.mock("../src/sms.js");
test("SMS matni va raqami", async () => {
await markReady({ id: 7, phone: "90 000 00 00" });
expect(sendSms).toHaveBeenCalledWith(
"+998 90 000 00 00",
"Buyurtma #7 tayyor",
);
}); FAIL sinov/order2.test.js > SMS matni va raqami
AssertionError: expected "vi.fn()" to be called with arguments: [ '+998 90 000 00 00', …(1) ]
Received:
1st vi.fn() call:
[
- "+998 90 000 00 00",
+ undefined,
"Buyurtma #7 tayyor",
]
Number of calls: 1- — kutilgan, + — haqiqiy. Raqam o'rniga undefined! Sabab: automock formatPhone ni ham soxtaga aylantirdi, u esa undefined qaytaradi. Bizga faqat sendSms soxta kerak edi.
4.2 Qisman soxta: importOriginal
Fabrika — modul o'rniga nima berilishi. importOriginal() asl modulni yuklaydi; hammasini olib, bittasini almashtiramiz:
vi.mock(import("../src/sms.js"), async (importOriginal) => ({
...(await importOriginal()),
sendSms: vi.fn().mockResolvedValue({ ok: true }),
}));vi.mock(import("…")) — yo'l import() ichida bo'lsa, TypeScript fabrika turlarini shu moduldan oladi (oddiy satr ham ishlaydi).
Shu misolni tayyorlayotganda haqiqiy voqea bo'ldi. Bizning birinchi formatPhone "+998 90 000 0000" qaytarardi (oxirgi to'rt raqamni bo'lmagan edi). Automock bu xatoni yashirdi — formatPhone umuman ishlamadi. Qisman soxta asl funksiyani qoldirdi va test darhol yiqildi:
[
- "+998 90 000 00 00",
+ "+998 90 000 0000",
"Buyurtma #7 tayyor",
]Saboq: keragidan ko'p narsani soxta qilmang. Har soxta — kodning sinalmagan bo'lagi.
4.3 Ko'tarilish (hoisting)
vi.mock qatorini fayl o'rtasiga yozsangiz ham, Vitest uni importlardan oldin, fayl boshiga ko'taradi. Aks holda order.js asl sms.js ni import qilib bo'lgan bo'lardi. Bu JavaScript'dagi var hoisting iga o'xshaydi, lekin uni Vitest kodni o'zgartirib qiladi.
sequenceDiagram
participant T as test fayli
participant V as Vitest
participant O as order.js
V->>T: vi.mock qatorini boshiga ko'taradi
T->>V: vi.mock("sms.js") — ro'yxatga
T->>O: import { markReady }
O->>V: import "./sms.js" so'raldi
V-->>O: soxta modul beriladi
T->>T: test ishlaydiKo'tarilishning narxi bor: fabrika test faylidagi oddiy o'zgaruvchilarni ko'rmaydi — ular hali e'lon qilinmagan:
const fakeSend = vi.fn();
vi.mock("../src/sms.js", () => ({
sendSms: fakeSend,
formatPhone: (raw) => raw,
}));Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
Caused by: ReferenceError: Cannot access 'fakeSend' before initializationTarjima: "Modulni soxtalashda xato. vi.mock fabrikasida yuqori darajadagi o'zgaruvchi bo'lmasin — bu chaqiruv fayl boshiga ko'tariladi." Pastdagi ReferenceError — haqiqiy sabab: fakeSend hali yaratilmagan (TDZ, let va const darsidagidek).
Yechim — vi.hoisted: uning ichidagi kod ham ko'tariladi va vi.mock dan oldin ishlaydi:
const { fakeSend } = vi.hoisted(() => ({ fakeSend: vi.fn() }));
vi.mock("../src/sms.js", () => ({
sendSms: fakeSend,
formatPhone: (raw) => raw,
}));Ko'pincha soddarog'i yetadi: import { sendSms } (soxtasi keladi) va vi.mocked(sendSms).mockResolvedValue(…) — vi.mocked faqat TypeScript turi uchun.
4.4 Natija oynasida: bog'liqlikni tashqaridan berish
Natija oynasida vi.mock yo'q (modul fayllari yo'q). U yerda xuddi shu g'oyani bog'liqlikni tashqaridan berish (dependency injection) bilan ko'rsatamiz — Fetch klient da tanishgan usul. Bog'liqlik parametr bilan keladi, sukut bo'yicha — haqiqiysi:
import { expect, test, vi } from "vitest";
function formatPhone(raw) {
const d = raw.replace(/\D/g, "").slice(-9);
const parts = [
d.slice(0, 2), d.slice(2, 5), d.slice(5, 7), d.slice(7),
];
return `+998 ${parts.join(" ")}`;
}
async function sendSms(phone, text) {
throw new Error("Testda haqiqiy SMS yo'q!"); // tarmoqqa chiqardi
}
// Bog'liqlik parametr bilan keladi; sukut bo'yicha — haqiqiysi
async function markReady(order, deps = { sendSms }) {
const phone = formatPhone(order.phone);
await deps.sendSms(phone, `Buyurtma #${order.id} tayyor`);
return { ...order, status: "tayyor" };
}
test("SMS raqami va matni", async () => {
const fakeSend = vi.fn().mockResolvedValue({ ok: true });
const done = await markReady(
{ id: 7, phone: "90 000 00 00" },
{ sendSms: fakeSend },
);
expect(done.status).toBe("tayyor");
expect(fakeSend).toHaveBeenCalledWith(
"+998 90 000 00 00",
"Buyurtma #7 tayyor",
);
});Konsolda:
✓ SMS raqami va matni
Testlar: 1 o'tdi (jami 1)Qaysi biri yaxshi? Kanon ikkalasini ishlatadi. Sof mantiqda (apiKlient({ fetch }), oflayn navbat) — parametr: test sodda, sehr yo'q. Kod o'zgartirib bo'lmaydigan joyda (masalan, React komponenti ichidagi import) — vi.mock. Kanon RTL II da til tanlash testida aynan qisman vi.mock ishlatadi.
Tekshirib ko'ring:
vi.mockni test ichiga,test(…)ning ichiga yozsangiz, u faqat shu testga ta'sir qiladimi?
Javob
Yo'q. vi.mock doim fayl boshiga ko'tariladi va butun fayl uchun ishlaydi. Vitest 5 chalg'itmaslik uchun bunday faylni umuman ishga tushirmaydi (biz sinadik, birinchi qator):
Error: 1 call in "sinov/inside.test.js" was defined outside of the module's top level scope:Tarjima: "fayldagi 1 ta chaqiruv modulning yuqori darajasidan tashqarida yozilgan". Davomida Vitest tushuntiradi: u baribir ko'tariladi, shuning uchun yuqoriga ko'chiring. Bitta test uchun boshqa xulq kerak bo'lsa — soxta funksiyaning o'zini o'sha testda sozlang: vi.mocked(sendSms).mockRejectedValueOnce(…).
5. Soxta taymerlar: vaqtni test boshqaradi
5.1 Debounce'ni bir zumda sinash
Debounce darsidagi qidiruv: foydalanuvchi yozishdan to'xtagach, 400 ms dan keyin bitta so'rov. Haqiqiy vaqt bilan sinasak, har test sekundlab kutadi va baribir beqaror. vi.useFakeTimers() — soxta taymerlar (fake timers): setTimeout, setInterval va Date.now soxta soatga ulanadi. Vaqt faqat siz aytganda yuradi:
import { afterEach, beforeEach, expect, test, vi } from "vitest";
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test("yozish to'xtagach 400 ms — bitta qidiruv", () => {
const search = vi.fn();
const onInput = debounce(search, 400);
onInput("o");
vi.advanceTimersByTime(100);
onInput("os");
vi.advanceTimersByTime(100);
onInput("osh");
vi.advanceTimersByTime(399);
expect(search).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(search).toHaveBeenCalledTimes(1);
expect(search).toHaveBeenCalledWith("osh");
});
test("runAllTimers — navbatdagi hamma taymer", () => {
const log = [];
setTimeout(() => log.push("osh tayyor"), 15 * 60 * 1000);
setTimeout(() => log.push("choy tayyor"), 3 * 60 * 1000);
console.log("navbatda:", vi.getTimerCount());
vi.runAllTimers();
console.log(log);
expect(vi.getTimerCount()).toBe(0);
});Konsolda:
✓ yozish to'xtagach 400 ms — bitta qidiruv
navbatda: 2
(2) ['choy tayyor', 'osh tayyor']
✓ runAllTimers — navbatdagi hamma taymer
Testlar: 2 o'tdi (jami 2)399 ms da qidiruv yo'q, 400 ms da bitta. Ikkinchi testda 15 daqiqa bir zumda o'tdi, taymerlar esa vaqt tartibida ishladi: avval choy (3 daqiqa).
| Buyruq | Nima qiladi |
|---|---|
vi.advanceTimersByTime(ms) |
soatni ms ga suradi, vaqti kelganlar ishlaydi |
vi.runAllTimers() |
navbat bo'shaguncha hammasi (yangi qo'shilganlari ham) |
vi.runOnlyPendingTimers() |
faqat hozir navbatdagilar |
vi.getTimerCount() |
navbatda nechta taymer |
vi.setSystemTime(sana) |
soxta "hozir" ni o'rnatadi |
setInterval bilan runAllTimers() cheksiz aylanadi — haqiqiy Vitest 10 000 taymerdan keyin to'xtaydi: Error: Aborting after running 10000 timers, assuming an infinite loop! ("cheksiz sikl deb hisoblab, 10 000 taymerdan keyin to'xtatildi"). Interval uchun advanceTimersByTime yoki runOnlyPendingTimers ni ishlating.
5.2 "Hozir" ni o'rnatish: ish vaqti
«Bahor» 07:00 dan 23:00 gacha ishlaydi. isOpen() ni ertalab ham, kechasi ham sinash uchun soatni kutmaymiz — vi.setSystemTime "hozir" ni o'rnatadi:
import { afterEach, expect, test, vi } from "vitest";
// Ish vaqti 07:00–23:00, Toshkent vaqti (UTC+5)
function isOpen(now = new Date()) {
const hour = (now.getUTCHours() + 5) % 24;
return hour >= 7 && hour < 23;
}
afterEach(() => {
vi.useRealTimers();
});
const cases = [
["06:59 — yopiq", "2026-11-02T01:59:00Z", false],
["07:00 — ochiq", "2026-11-02T02:00:00Z", true],
["22:59 — ochiq", "2026-11-02T17:59:00Z", true],
["23:00 — yopiq", "2026-11-02T18:00:00Z", false],
];
for (const [name, iso, expected] of cases) {
test(name, () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(iso));
expect(isOpen()).toBe(expected);
});
}Konsolda:
✓ 06:59 — yopiq
✓ 07:00 — ochiq
✓ 22:59 — ochiq
✓ 23:00 — yopiq
Testlar: 4 o'tdi (jami 4)Chegaraviy holatlar (D-59 darsidagidek) — aynan 07:00 va 23:00: off-by-one xato (hour <= 23) shu yerda ushlanadi.
setSystemTime dan keyin new Date() ham, Date.now() ham soxta vaqtni beradi. Toshkent vaqtini qo'lda hisobladik: test kimning kompyuterida ishlashidan qat'i nazar bir xil natija bersin (Date va vaqt zonalari).
5.3 Kanon topgan ikki tuzoq
1. Faqat aytilgani soxtalanadi. node:test dagi mock.timers.enable({ apis: ["setTimeout"] }) clearTimeout ni ham soxtalaydi. Vitest'dagi toFake: ["setTimeout"] esa — faqat setTimeout. Kanon ko'chirishda kechiktir (debounce) testi shu sabab yiqildi: haqiqiy clearTimeout soxta taymerni bekor qila olmadi. O'zingiz takrorlang:
import { afterEach, expect, test, vi } from "vitest";
afterEach(() => {
vi.useRealTimers();
});
function cancelled(toFake) {
vi.useFakeTimers({ toFake });
const f = vi.fn();
const id = setTimeout(f, 100);
clearTimeout(id);
vi.advanceTimersByTime(200);
return f;
}
test("faqat setTimeout soxta", () => {
expect(cancelled(["setTimeout"])).not.toHaveBeenCalled();
});
test("setTimeout va clearTimeout soxta", () => {
const f = cancelled(["setTimeout", "clearTimeout"]);
expect(f).not.toHaveBeenCalled();
});Konsolda:
× faqat setTimeout soxta
→ expected "vi.fn()" to not be called at all, but actually been called 1 times
✓ setTimeout va clearTimeout soxta
Testlar: 1 yiqildi, 1 o'tdi (jami 2)Birinchi testda haqiqiy clearTimeout soxta taymerni topa olmadi — f baribir chaqirildi. Haqiqiy Vitest 5.0.4 da xabar aynan shu (biz sinadik). To'g'risi — toFake: ["setTimeout", "clearTimeout"] yoki toFake siz (sukut bo'yicha hammasi).
Soxta soat vi.useRealTimers() gacha yoqiq turadi va keyingi testga ham o'tadi — shuning uchun u doim afterEach da.
2. Soxta soatni kech yoqing. Kanonning qoralama testida soat render tugagach yoqiladi. Sababi izohda: TanStack Query va Testing Library'ning waitFor haqiqiy taymerlarga tayanadi — soat oldin to'xtatilsa, waitFor abadiy kutadi.
6. Har test toza boshlansin: beforeEach / afterEach
6.1 Tartib
Vitest I da describe bilan guruhlagan edik. Har guruhning o'z "oldin" va "keyin" ilmoqlari (hook) bor:
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
test,
} from "vitest";
beforeAll(() => console.log("1. beforeAll — boshida bir marta"));
beforeEach(() => console.log("2. beforeEach — tashqi"));
afterEach(() => console.log("5. afterEach — tashqi"));
afterAll(() => console.log("6. afterAll — oxirida bir marta"));
describe("savat", () => {
beforeEach(() => console.log("3. beforeEach — ichki"));
afterEach(() => console.log("4. afterEach — ichki"));
test("osh qo'shildi", () => console.log(" test 1"));
test("choy qo'shildi", () => console.log(" test 2"));
});Konsolda:
1. beforeAll — boshida bir marta
2. beforeEach — tashqi
3. beforeEach — ichki
test 1
4. afterEach — ichki
5. afterEach — tashqi
✓ savat > osh qo'shildi
2. beforeEach — tashqi
3. beforeEach — ichki
test 2
4. afterEach — ichki
5. afterEach — tashqi
✓ savat > choy qo'shildi
6. afterAll — oxirida bir marta
Testlar: 2 o'tdi (jami 2)beforeEach — tashqaridan ichkariga, afterEach — teskari: ichkaridan tashqariga (kiyim kabi: oxirgi kiyilgani birinchi yechiladi). Haqiqiy Vitest'da tartib xuddi shunday (tekshirdik). ✓ — test va uning afterEach lari tugagach.
6.2 Tozalashning uch darajasi
| Metod | mock.calls |
Xulq (mockReturnValue…) |
spyOn'dagi asl metod |
|---|---|---|---|
mockClear() |
tozalanadi | qoladi | almashgan holda |
mockReset() |
tozalanadi | boshlang'ich holatga | almashgan holda |
mockRestore() |
tozalanadi | boshlang'ich holatga | asl joyiga qaytadi |
vi.clearAllMocks(), vi.resetAllMocks(), vi.restoreAllMocks() — hamma soxtalar uchun. Vitest 3 dan beri restoreAllMocks faqat vi.spyOn josuslarini tiklaydi; oddiy vi.fn() ning tarixiga tegmaydi. Har faylda yozmaslik uchun vitest.config.ts da bayroqlar bor: restoreMocks: true, unstubGlobals: true. Kanon esa tozalashni har faylda ochiq yozadi — nima tiklanayotgani ko'rinib tursin.
7. test.each: bitta test — ko'p ma'lumot
Algoritmni testlash darsida jadvalli testni (table-driven test) qo'lda sikl bilan yozgan edik. Vitest'da buning tayyor shakli bor. Sarlavhada $nom — obyekt maydoni, %i — butun son, %s — satr:
import { describe, expect, test } from "vitest";
function deliveryFee(total, km) {
if (km > 15) throw new Error("15 km dan uzoqqa yetkazmaymiz");
if (total >= 150000) return 0;
return km <= 3 ? 10000 : 10000 + (km - 3) * 2000;
}
describe("deliveryFee", () => {
test.each([
{ total: 70000, km: 2, fee: 10000 },
{ total: 70000, km: 3, fee: 10000 },
{ total: 70000, km: 8, fee: 20000 },
{ total: 150000, km: 8, fee: 0 },
])("$total so'm, $km km → $fee", ({ total, km, fee }) => {
expect(deliveryFee(total, km)).toBe(fee);
});
test.each([[16], [40]])("%i km — rad etiladi", (km) => {
expect(() => deliveryFee(70000, km)).toThrow("15 km");
});
});Konsolda:
✓ deliveryFee > 70000 so'm, 2 km → 10000
✓ deliveryFee > 70000 so'm, 3 km → 10000
✓ deliveryFee > 70000 so'm, 8 km → 20000
✓ deliveryFee > 150000 so'm, 8 km → 0
✓ deliveryFee > 16 km — rad etiladi
✓ deliveryFee > 40 km — rad etiladi
Testlar: 6 o'tdi (jami 6)Har qator — alohida test: bittasi yiqilsa, qaysi qatorligi sarlavhadan ko'rinadi. Haqiqiy Vitest'da shablon satrli jadval (test.each`…`) ham bor (natija oynasidagi modelda — faqat massiv).
8. Snapshot: natija nusxasini saqlash
Snapshot test — natijani bir marta faylga yozib, keyingi ishga tushirishlarda u bilan solishtirish. toMatchSnapshot() — alohida faylga, toMatchInlineSnapshot() — test faylining o'ziga. Natija oynasida snapshot yo'q (yoziladigan fayl yo'q) — quyidagilar haqiqiy terminaldan:
const items = [
{ name: "Osh", qty: 2, price: 35000 },
{ name: "Ko'k choy", qty: 1, price: 5000 },
];
test("chek — snapshot faylga", () => {
expect(receipt(items)).toMatchSnapshot();
});
test("chek — inline snapshot", () => {
expect(receipt(items)).toMatchInlineSnapshot();
});Birinchi ishga tushirishda Vitest Snapshots 2 written deydi va toMatchInlineSnapshot() qavsi ichiga natijani o'zi yozib qo'yadi (test fayli o'zgaradi). Birinchisi __snapshots__/receipt.test.js.snap fayliga tushadi (u ham git'ga kiradi). Endi chek qatoriga " so'm" qo'shsak:
Error: Snapshot `chek — snapshot faylga 1` mismatched
- Expected
+ Received
{
"lines": [
- "Osh x2 — 70000",
+ "Osh x2 — 70000 so'm",
- "Ko'k choy x1 — 5000",
+ "Ko'k choy x1 — 5000 so'm",
],
"total": 75000,
}O'zgarish ataylab bo'lsa — npx vitest run -u (--update): Snapshots 2 updated. CI'da esa yangi snapshot yozilmaydi: biz CI=1 bilan sinab ko'rdik — .snap fayli yo'q test Snapshot … mismatched bilan yiqildi. To'g'ri: snapshot'ni lokal yozib, commit qilish.
Qachon foydali. Kichik, barqaror natija (chek matni, xato xabari) — yaxshi. 300 qatorli komponent HTML'i — yomon: diff'ni hech kim o'qimaydi, hamma -u bosadi va test hech narsani himoya qilmaydi. Kanon vazifalar da snapshot test yo'q: React komponentlarini RTL I dagi so'rovlar bilan tekshiramiz — ular nima muhimligini aniq aytadi. Komponentning ko'rinishini rasm bilan solishtirish — Visual regression da.
9. Coverage: qaysi kod ishlamadi?
9.1 O'rnatish va o'qish
Qamrov (coverage) — testlar ishga tushirgan kod ulushi. Vitest'da uni @vitest/coverage-v8 yig'adi: V8 dvigatelining o'zi qaysi kod ishlaganini sanaydi, kodni o'zgartirmaydi. Muqobil @vitest/coverage-istanbul kodga hisoblagichlar qo'shadi — eski loyihalarda uchraydi.
npm i -D -E @vitest/coverage-v8@5.0.4
npx vitest run --coverageKichik misol — deliveryFee va etaMinutes bo'lgan delivery.js, faqat ikkita test bilan (70000, 2 va 200000, 8):
% Coverage report from v8
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 66.66 | 66.66 | 50 | 66.66 |
delivery.js | 66.66 | 66.66 | 50 | 66.66 | 4,13
-------------|---------|----------|---------|---------|-------------------| Ustun | Ma'nosi |
|---|---|
% Stmts (ifoda) |
ishlagan ifodalar |
% Branch (tarmoq) |
if/?: ning har ikki yo'li |
% Funcs (funksiya) |
kamida bir marta chaqirilgan funksiyalar |
% Lines (satr) |
ishlagan satrlar |
Uncovered Line #s |
hech ishlamagan satrlar |
4-satr — throw (16 km li test yo'q), 13-satr — etaMinutes (umuman chaqirilmagan). Diqqat qiling: km <= 3 ? … : … satri ro'yxatda yo'q — satr ishlagan, lekin uning : yo'li hech qachon bajarilmagan. Buni faqat tarmoq ustuni ko'radi 66.66-bob. reporter: ["html"] esa coverage/index.html ni yasaydi: brauzerda har satr yashil yoki qizil bo'yalgan.
9.2 Chegara (thresholds)
Qamrov o'zi hech narsani to'xtatmaydi. thresholds qo'shsak — chegaradan past bo'lsa, buyruq yiqiladi (chiqish kodi 1, CI qizil):
coverage: {
provider: "v8",
include: ["src/**/*.js"],
reporter: ["text", "html"],
thresholds: { lines: 80, branches: 80 },
},ERROR: Coverage for lines (66.66%) does not meet global threshold (80%)
ERROR: Coverage for branches (66.66%) does not meet global threshold (80%)Uchta test qo'shdik (8 km, 16 km, etaMinutes(5)) — hamma ustun 100. Lekin 100 % — xatosizlik emas. Aynan 150 000 so'm ham, 3 km ham sinalmagan: >= o'rniga > yozilsa, qamrov baribir 100 % qolardi. Qamrov "qaysi kod umuman tekshirilmagan" degan savolga javob beradi, "testlar yaxshimi" degan savolga emas (Nega test yozamiz).
Tekshirib ko'ring:
% Lines100,% Branch50. Bu nimani bildiradi?
Javob
Har satr kamida bir marta ishlagan, lekin shartlarning yarim yo'llari hech qachon bajarilmagan. Masalan, if ning faqat "ha" holati sinalgan, "yo'q" holati — yo'q. Bitta satrga yozilgan a ? b : c ham shunday: satr ishlaydi, c esa hech qachon.
10. Vazifalar qadami: coverage va chegara (holat 03)
10.1 O'rnatish
git switch -c chore/qamrov
npm i -D -E @vitest/coverage-v8@5.0.4Narx: +1 MB node_modules, 8 paket. Ilova bundle'i o'zgarmaydi — bu faqat test vositasi (birinchi yuklash JS 206.7 KB gzip, oldingi holatdagidek). package.json ga skript:
"test:qamrov": "vitest run --project mantiq --project komponent --coverage",coverage/ papkasi — .gitignore, ESLint (globalIgnores) va Prettier'da: bu yig'iladigan hisobot, kod emas.
10.2 vitest.config.ts
Kanon (qo'shilgan qism):
// O'lchangan qamrov (22/#03, 61.3 / 57.6 / 55.6 / 61.2 %) dan
// 1–2 punkt past: yangi kod testsiz qo'shilsa —
// npm run test:qamrov yiqiladi
const QAMROV_CHEGARASI = {
lines: 60,
functions: 55,
branches: 53,
statements: 60,
}; // 22/#03: coverage — V8 o'zi yig'adi (kodni o'zgartirmaydi).
// npm run test:qamrov → coverage/index.html. Chegara
// (thresholds) — o'lchangan qiymatdan biroz past: tushib
// ketsa test yiqiladi
coverage: {
provider: "v8",
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.d.ts", "src/main.tsx"],
reporter: ["text-summary", "html"],
thresholds: QAMROV_CHEGARASI,
},include — faqat ilova kodi (src/). Testlar tegmagan fayllar ham 0 % bilan kiradi — aks holda testsiz fayl jadvalda ko'rinmasdi va raqam yolg'on chiroyli bo'lardi. main.tsx (faqat ilovani ulaydi) va .d.ts (faqat turlar) — chiqarilgan.
Nega chegara 80 emas, 60? Chegara — pastga tushmaslik uchun: o'lchangan qiymatdan 1–2 punkt past. Yuqori raqamni birdan talab qilsak, hamma "qamrov uchun" ma'nosiz test yoza boshlaydi.
10.3 Raqamlar
Avval mavjud 320 test bilan o'lchandi, keyin ikki yangi test fayli qo'shildi:
| % | holat 02 (320 test) | holat 03 (327 test) |
|---|---|---|
| satr | 60.30 | 61.28 |
| tarmoq | 54.73 | 55.59 |
| funksiya | 56.34 | 57.56 |
| ifoda | 60.28 | 61.21 |
Hisobot 27 ta 0 % faylni ko'rsatdi: buyruqlar oynasi, tartiblash rejimi, sahifalar, kuzatuv/xatolar.ts, RasmKesish va boshqalar. App.tsx — 38.6 %. Dialog va buyruqlar oynasi Browser Mode da, sahifalar Playwright da yopiladi.
Vaqt: npm test 8.06 s, npm run test:qamrov 10.68 s (coverage bilan, issiq). Yangi paketdan keyingi birinchi (sovuq) ishga tushirish — Vitest I dagidek ancha sekin.
10.4 Ikki yangi test fayli
sw-yangilanish.test.js (6 test) — PWA darsidagi "Yangi versiya tayyor" oqimi, brauzersiz. navigator.serviceWorker, document, location — vi.stubGlobal bilan; Service Worker — EventTarget va vi.fn() dan yasalgan soxta obyekt (postMessage: vi.fn(), location.reload: vi.fn()).
Modulning o'zida holat bor (kutayotgan SW, "foydalanuvchi so'radimi"). Har test yangi nusxa olishi uchun — vi.resetModules() va dinamik import():
const modul = () => import("../src/sw-yangilanish.ts");
beforeEach(() => vi.resetModules());
afterEach(() => vi.unstubAllGlobals());Fayl qamrovi 0 → 80.8 %. Testlardan biri: boshqa tabdan kelgan controllerchange sahifani qayta yuklamaydi — yozilayotgan matn yo'qolmaydi.
qoralama.test.tsx (1 test) — Debounce dagi qoralama: 3 harf → 400 ms dan keyin localStorage ga bitta yozuv. Uchta vosita birga: vi.spyOn(Storage.prototype, "setItem"), soxta soat (render tugagach) va toFake ikkala funksiya bilan:
const yozuv = vi.spyOn(Storage.prototype, "setItem");
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
for (const matn of ["N", "No", "Non"]) {
fireEvent.input(maydon, { target: { value: matn } });
vi.advanceTimersByTime(100);
}
// … qoralamaYozuvlari() — yozuv.mock.calls dan shu kalitlilari
vi.advanceTimersByTime(299); // oxirgi harfdan 399 ms
expect(qoralamaYozuvlari()).toEqual([]);
vi.advanceTimersByTime(1);
expect(qoralamaYozuvlari()).toEqual([["vazifalar:qoralama", "Non"]]);Testning o'zi sinaldi: kanon QORALAMA_KUTISH_MS ni vaqtincha 0 qildi — test yiqildi, ya'ni u haqiqatan debounce'ni tekshiradi. Foydali odat: yangi testni bir marta ataylab buzib ko'ring.
10.5 Natija va commit
Testlar 327/327, test:qamrov — chegaradan o'tdi; lint, format:check, tip, knip — toza. JS 206.7 KB — o'zgarmadi.
git add -A
git commitCommit (kanon): test: coverage-v8 va chegara; SW yangilanishi va qoralama debounce testlari.
11. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| testlar alohida o'tadi, birga yiqiladi | soxta xulq keyingi testga oqdi | afterEach da restoreAllMocks() |
toHaveBeenCalledWith da undefined |
automock boshqa funksiyani ham o'chirdi | importOriginal bilan qisman |
Cannot access '…' before initialization |
vi.mock fabrikasi ko'tarildi |
vi.hoisted yoki vi.mocked |
stub ishlamaydi, haqiqiy fetch ketdi |
modul globalni yuklanganda olgan | stub'ni sozlash fayliga |
clearTimeout taymerni to'xtatmaydi |
toFake da faqat setTimeout |
ikkalasini yozing |
waitFor abadiy kutadi |
soxta soat juda erta | render tugagach yoqing |
Aborting after running 10000 timers |
setInterval + runAllTimers |
advanceTimersByTime |
| snapshot CI'da yiqiladi | .snap commit qilinmagan |
lokal yozib, commit |
12. Mashqlar
Mashqlar kurs/mashqlar/22/03-mock-taymer/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.
1-mashq (oson): bo'sh joylarni to'ldiring
- Chaqiruvlar argumentlari:
send.mock. - Faqat keyingi chaqiruvda qiymat:
mockReturnValue - Mavjud metodni kuzatish:
vi.(sms, "send") - Soatni 400 ms ga surish:
vi.(400) - Snapshot'ni yangilash bayrog'i:
npx vitest run -
Yechim
- calls — har chaqiruv argumentlari massivi; natijalar —
mock.results. - Once — navbatga turadi; navbat tugasa,
mockReturnValuedagi doimiy qiymat yokiundefined. - spyOn — asl metod ishlayveradi, kerak bo'lsa
mockImplementation. - advanceTimersByTime —
vi.useFakeTimers()dan keyin ishlaydi; usiz xato beradi. - u —
--update; faqat o'zgarish ataylab bo'lsa.
2-mashq (o'rta): eslatma bir marta ketsin
scheduleReminder(order, send) buyurtmadan 20 daqiqa keyin send(order.phone, "Buyurtmangiz yo'lda") ni chaqiradi va bekor qilish funksiyasini qaytaradi. Ikki test yozing: (1) 19:59 da hali yo'q, 20:00 da bitta; (2) 5-daqiqada bekor qilinsa — 30 daqiqadan keyin ham yo'q. Soxta soatni beforeEach da yoqing, afterEach da o'chiring.
Ishora: bekor qilish — clearTimeout (Taymerlar); daqiqa — 60 * 1000 ms.
Yechim
import { afterEach, beforeEach, expect, test, vi } from "vitest";
const MIN = 60 * 1000;
function scheduleReminder(order, send) {
const id = setTimeout(
() => send(order.phone, "Buyurtmangiz yo'lda"),
20 * MIN,
);
return () => clearTimeout(id);
}
const order = { id: 9, phone: "+998 90 000 00 09" };
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test("20 daqiqadan keyin bitta eslatma", () => {
const send = vi.fn();
scheduleReminder(order, send);
vi.advanceTimersByTime(20 * MIN - 1);
expect(send).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(send).toHaveBeenCalledTimes(1);
expect(send).toHaveBeenCalledWith(
order.phone,
"Buyurtmangiz yo'lda",
);
});
test("bekor qilinsa — eslatma yo'q", () => {
const send = vi.fn();
const cancel = scheduleReminder(order, send);
vi.advanceTimersByTime(5 * MIN);
cancel();
vi.advanceTimersByTime(30 * MIN);
expect(send).not.toHaveBeenCalled();
});Konsolda:
✓ 20 daqiqadan keyin bitta eslatma
✓ bekor qilinsa — eslatma yo'q
Testlar: 2 o'tdi (jami 2)20 * MIN - 1 — chegaradan bir ms oldin: "aynan 20 daqiqada" ni shunday isbotlaymiz.
3-mashq (qiyin): kesh va josus
createMenuCache(fetchMenu, ttlMs) — get() metodli obyekt qaytaradi. Birinchi get() fetchMenu() ni chaqiradi va natijani eslab qoladi. ttlMs o'tmaguncha keyingi get() lar serverga bormaydi. Muddat o'tsa — qayta so'raydi. Server xato bersa — kesh to'lmaydi, keyingi get() yana urinadi. Testlar: kesh ishlaydi (2 ta get → 1 so'rov), muddat o'tgach yangi so'rov, xatodan keyin qayta urinish (mockRejectedValueOnce). Vaqtni Date.now() dan o'qing.
Ishora: vi.fn().mockResolvedValueOnce(…) ketma-ket javoblar beradi; soxta soatda Date.now() ham suriladi.
Yechim
import { afterEach, beforeEach, expect, test, vi } from "vitest";
function createMenuCache(fetchMenu, ttlMs) {
let value = null;
let savedAt = 0;
return {
async get() {
const fresh = value !== null && Date.now() - savedAt < ttlMs;
if (fresh) return value;
value = await fetchMenu();
savedAt = Date.now();
return value;
},
};
}
const v1 = [{ name: "Osh", price: 35000 }];
const v2 = [{ name: "Osh", price: 37000 }];
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test("muddat ichida — bitta so'rov", async () => {
const fetchMenu = vi.fn().mockResolvedValue(v1);
const cache = createMenuCache(fetchMenu, 60000);
await cache.get();
vi.advanceTimersByTime(59999);
expect(await cache.get()).toEqual(v1);
expect(fetchMenu).toHaveBeenCalledTimes(1);
});
test("muddat o'tgach — yangi narx", async () => {
const fetchMenu = vi
.fn()
.mockResolvedValueOnce(v1)
.mockResolvedValueOnce(v2);
const cache = createMenuCache(fetchMenu, 60000);
await cache.get();
vi.advanceTimersByTime(60000);
expect(await cache.get()).toEqual(v2);
expect(fetchMenu).toHaveBeenCalledTimes(2);
});
test("xatodan keyin — kesh bo'sh, qayta urinadi", async () => {
const fetchMenu = vi
.fn()
.mockRejectedValueOnce(new Error("Server javob bermadi"))
.mockResolvedValueOnce(v1);
const cache = createMenuCache(fetchMenu, 60000);
await expect(cache.get()).rejects.toThrow("Server javob bermadi");
expect(await cache.get()).toEqual(v1);
expect(fetchMenu).toHaveBeenCalledTimes(2);
});Konsolda:
✓ muddat ichida — bitta so'rov
✓ muddat o'tgach — yangi narx
✓ xatodan keyin — kesh bo'sh, qayta urinadi
Testlar: 3 o'tdi (jami 3)59 999 ms — chegaradan bir ms oldin, 60 000 — aynan chegara. Uchinchi test kesh xatoni eslab qolmasligini isbotlaydi: aks holda mehmonlar bir daqiqa bo'sh menyu ko'rardi.
4-mashq: Vazifalar qadami
chore/qamrov branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: @vitest/coverage-v8@5.0.4, test:qamrov skripti, vitest.config.ts dagi coverage va chegara, coverage/ — .gitignore, ESLint va Prettier'ga. Keyin qoralama debounce testini yozing va uni bir marta ataylab buzib ko'ring.
Yechim
Tekshiruv ro'yxati:
npm run test:qamrovoxirida jami qatori vacoverage/index.htmlbor; brauzerda oching —App.tsxning qizil satrlari ko'rinadi.- Chegara ishlaydi:
QAMROV_CHEGARASI.linesni vaqtincha 90 qiling — buyruqERROR: Coverage for lines … does not meet global threshold (90%)bilan yiqiladi. Qaytaring. qoralama.test.tsxo'tadi;QORALAMA_KUTISH_MSni 0 qilsangiz — yiqiladi (birinchitoEqual([])da). Qaytaring.git statusdacoverage/ko'rinmaydi;npm run lintvanpm run format:check— toza.npm run build— birinchi yuklash JS o'zgarmagan (coverage — faqat devDependency).
13. Real ishda
- Har loyihada: to'lov, SMS, analitika,
fetch— testda doim soxta. Haqiqiy tarmoq testlarinpm testdan tashqarida (kanondatest:tarmoq). - CI'da coverage chegarasi — "qizil qurilish" sabablaridan biri. Codecov kabi xizmatlar PR'da qamrov o'zgarishini ko'rsatadi (hisob kerak, kanonda ishlatilmagan).
- Intervyu savollari: "mock, stub va spy farqi?", "
vi.mocknega ko'tariladi?", "debounce'ni qanday sinaysiz?", "100 % coverage yetarlimi?".
Xulosa
vi.fn()— chaqiruvlarni yozadigan soxta funksiya; xulqmockReturnValue,mockResolvedValue,Oncenavbati bilan.vi.spyOn— mavjud metodni kuzatadi (asl ishlaydi),vi.stubGlobal— global; ikkalasiniafterEachda tiklang.vi.mockfayl boshiga ko'tariladi; keragidan ko'p soxtalamang —importOriginalbilan qisman; oddiy holatda bog'liqlikni parametr bilan bering.- Soxta taymerlar vaqtni test qo'liga beradi: chegaradan 1 ms oldin va aynan chegarada tekshiring.
test.each— jadvalli test; snapshot — kichik, barqaror natija uchun.- Coverage — tekshirilmagan kod xaritasi; chegara pastga tushmaslik uchun, 100 % esa xatosizlik emas.
vazifalar: 60.30 → 61.28 %, 327 test.
Keyingi dars: React Testing Library I: render va so'rovlar — komponentni chizib, elementlarni foydalanuvchi ko'rganidek — roli va nomi bilan — topish va jest-dom matcherlari.
Manbalar
- Vitest: Mocking (vitest.dev/guide/mocking),
viAPI —vi.fn,vi.spyOn,vi.mock,vi.hoisted,vi.useFakeTimers(vitest.dev/api/vi) - Vitest: Snapshot (vitest.dev/guide/snapshot), Coverage (vitest.dev/guide/coverage)
- Vitest 3 migratsiya qo'llanmasi:
restoreAllMocksvamockResetxulqi (vitest.dev/guide/migration) - Martin Fowler, "Mocks Aren't Stubs" (martinfowler.com) — test dublyorlari turlari
- Haqiqiy chiqishlar: Vitest 5.0.4,
@vitest/coverage-v85.0.4, Node 24.21.0, Windows 11 (2026-10-20)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!