IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash3/16-dars30 daqiqa
Mundarija (40)

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 daqiqa vi.advanceTimersByTime() bilan bir zumda o'tadi. Har testdan keyin hammasi afterEach da 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.spyOn va vi.stubGlobal bilan mavjud metod va global qiymatni vaqtincha almashtirasiz.
  • vi.mock qanday 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:

jsx
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:

text
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: notifyReady ga bo'sh massiv bersak, qaysi matcher to'g'ri: expect(send).not.toHaveBeenCalled() yoki expect(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:

jsx
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:

text
  ✓ 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:

jsx
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:

text
[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:

jsx
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:

text
  ✓ 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, keyin Date.now ni 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:

js
// 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):

js
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",
  );
});
text
 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:

js
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:

text
  [
-   "+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 ishlaydi

Ko'tarilishning narxi bor: fabrika test faylidagi oddiy o'zgaruvchilarni ko'rmaydi — ular hali e'lon qilinmagan:

js
const fakeSend = vi.fn();
vi.mock("../src/sms.js", () => ({
  sendSms: fakeSend,
  formatPhone: (raw) => raw,
}));
text
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 initialization

Tarjima: "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:

js
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:

jsx
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:

text
  ✓ 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.mock ni 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):

text
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:

jsx
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:

text
  ✓ 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:

jsx
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:

text
  ✓ 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:

jsx
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:

text
  × 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:

jsx
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:

text
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:

jsx
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:

text
  ✓ 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:

js
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:

text
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.

bash
npm i -D -E @vitest/coverage-v8@5.0.4
npx vitest run --coverage

Kichik misol — deliveryFee va etaMinutes bo'lgan delivery.js, faqat ikkita test bilan (70000, 2 va 200000, 8):

text
 % 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):

ts
coverage: {
  provider: "v8",
  include: ["src/**/*.js"],
  reporter: ["text", "html"],
  thresholds: { lines: 80, branches: 80 },
},
text
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: % Lines 100, % Branch 50. 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

bash
git switch -c chore/qamrov
npm i -D -E @vitest/coverage-v8@5.0.4

Narx: +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:

json
"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):

ts
// 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,
};
ts
    // 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():

js
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:

tsx
  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.

bash
git add -A
git commit

Commit (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, mockReturnValue dagi doimiy qiymat yoki undefined.
  • 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
jsx
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:

text
  ✓ 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
jsx
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:

text
  ✓ 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:qamrov oxirida jami qatori va coverage/index.html bor; brauzerda oching — App.tsx ning qizil satrlari ko'rinadi.
  • Chegara ishlaydi: QAMROV_CHEGARASI.lines ni vaqtincha 90 qiling — buyruq ERROR: Coverage for lines … does not meet global threshold (90%) bilan yiqiladi. Qaytaring.
  • qoralama.test.tsx o'tadi; QORALAMA_KUTISH_MS ni 0 qilsangiz — yiqiladi (birinchi toEqual([]) da). Qaytaring.
  • git status da coverage/ ko'rinmaydi; npm run lint va npm 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 testlari npm test dan tashqarida (kanonda test: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.mock nega ko'tariladi?", "debounce'ni qanday sinaysiz?", "100 % coverage yetarlimi?".

Xulosa

  • vi.fn() — chaqiruvlarni yozadigan soxta funksiya; xulq mockReturnValue, mockResolvedValue, Once navbati bilan.
  • vi.spyOn — mavjud metodni kuzatadi (asl ishlaydi), vi.stubGlobal — global; ikkalasini afterEach da tiklang.
  • vi.mock fayl boshiga ko'tariladi; keragidan ko'p soxtalamang — importOriginal bilan 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), vi API — 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: restoreAllMocks va mockReset xulqi (vitest.dev/guide/migration)
  • Martin Fowler, "Mocks Aren't Stubs" (martinfowler.com) — test dublyorlari turlari
  • Haqiqiy chiqishlar: Vitest 5.0.4, @vitest/coverage-v8 5.0.4, Node 24.21.0, Windows 11 (2026-10-20)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Vitest mock va spy: vi.fn, vi.spyOn, vi.mock, soxta taymerlar va coverage — IlmHamroh