IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash15/16-dars26 daqiqa
Mundarija (36)

Test strategiyasi: nimani soxtalash, test ma'lumotlari fabrikasi va flaky testlar

Qisqacha: Ishonchli test to'plamining uch sharti bor. (1) Faqat chegarani soxtalang: tarmoq, vaqt, tasodif, brauzerda yo'q API — o'z mantiqingizni emas. (2) Test ma'lumotini fabrika yaratsin: vazifa({ matn: "Non" }) — faqat muhim maydon yoziladi, qolgani mantiqiy sukut. (3) Beqaror (flaky) testning sababini tuzating, qayta urinish bilan yashirmang: vaqtga tayanish o'rniga findBy/waitFor, "hozir" o'rniga setSystemTime, umumiy holat o'rniga har testga yangi ma'lumot. Coverage — tekshirilmagan kod xaritasi, sifat o'lchovi emas.

Bu darsda

  • Qaysi bog'liqlikni soxtalash, qaysini haqiqiy qoldirish kerakligini aniqlaysiz.
  • Test ma'lumotlari fabrikasini yozasiz; @faker-js/faker va fishery qachon kerakligini bilasiz.
  • Flaky testning to'rt asosiy sababini qayta yaratib, har birini tuzatasiz.
  • Flaky testni topish (yuk, --sequence.shuffle, --repeat-each) va karantin tartibini qo'llaysiz.
  • Test tezligini o'lchaysiz, CI'dagi o'rnini va coverage chegarasini to'g'ri baholaysiz.

Oldin bilishingiz kerak: Nozik xatolar: race condition, flaky test, Vitest II, RTL II, MSW, Playwright II.

1. Nega bu kerak?

Bu qismda vazifalar ga 349 ta Vitest testi, 21 ta e2e, 12 ta story va 6 ta vizual test yig'ildi. Hammasi yashil. Endi kanon ularni yuk ostida ishga tushirdi: bir vaqtda ikkita Vitest jarayoni, kompyuter band. Uch urinishdan birida 2 ta test yiqildi. Kod o'zgarmagan edi.

Bunday test flaky (beqaror) deyiladi: kod bir xil, natija — ba'zan yashil, ba'zan qizil. Nozik xatolar darsida biz uning Node'dagi sabablarini ko'rgan edik. Frontendda ular ko'proq: async render, animatsiya, sekin import, brauzer.

Flaky test nimasi bilan xavfli? Jamoa "e, bu test doim shunaqa" deb qayta ishga tushirishga o'rganadi. Bir kun haqiqiy xato ham xuddi shunday qizil bo'ladi — va uni ham qayta ishga tushirishadi. Testga ishonch yo'qolsa, testning foydasi qolmaydi.

Bu dars — butun qismning yakuni: qaysi testni qayerda yozish, nimani soxtalash, ma'lumotni qanday yaratish va beqarorlikni qanday yo'qotish. Misollarning markazida — vazifalar da haqiqatan topilgan va tuzatilgan flaky testlar.

vazifalar v12: testlar qavatlar bo'yicha
  • Vitest (unit + komponent)349 test
  • Playwright e2e21 test
  • Storybook12 test
  • Browser Mode6 test
  • Vizual6 test

Manba: vazifalar v12, holat 15 (2026-10-20)

Rasmda — test trofeyi (Nega test yozamiz) amalda: eng ko'p test arzon qavatda (Vitest, 6–10 s), qimmat qavatlarda — eng muhim yo'llar.

2. Nimani soxtalash kerak

2.1 Qoida: chegarada soxtalang

Soxta (mock) — test ichida haqiqiy bog'liqlik o'rniga turadigan narsa (Vitest II). U testni tez va barqaror qiladi, lekin soxtalangan kod sinalmaydi. Shuning uchun savol bitta: bu bog'liqlik testni sekin yoki beqaror qiladimi?

Soxtalang Haqiqiy qoldiring
tarmoq — MSW (MSW) o'z sof funksiyalaringiz (amallar.ts, hisob-kitob)
vaqt — vi.useFakeTimers, setSystemTime o'z komponentlaringiz va hooklaringiz
tasodif — Math.random, crypto.randomUUID React, router, i18n (providerlar bilan, RTL III)
jsdom'da yo'q API — matchMedia, IntersectionObserver brauzerda bor narsa — Browser Mode'da haqiqiysi
to'lov, SMS, analitika DOM va hodisalar

Kanon shu qoidaga amal qildi. vitest-sozlash.ts da faqat chegara soxtalangan: tarmoq (MSW server), matchMedia (jsdom'da yo'q). Qolgani haqiqiy.

2.2 O'z mantiqingizni soxtalasangiz

Savat komponenti jamini calcTotal dan oladi. Test yozuvchi "komponentni alohida sinash kerak" deb calcTotal ni ham soxtaladi:

jsx
import { render, screen } from "@testing-library/react";
import { expect, test, vi } from "vitest";

// Xato: qty hisobga olinmagan
function calcTotal(items) {
  return items.reduce((s, i) => s + i.price, 0);
}

function CartSummary({ items, total = calcTotal }) {
  return <p>Jami: {total(items)} so'm</p>;
}

const items = [
  { name: "Osh", price: 35000, qty: 2 },
  { name: "Ko'k choy", price: 5000, qty: 1 },
];

test("soxta calcTotal bilan", () => {
  const total = vi.fn().mockReturnValue(75000);
  render(<CartSummary items={items} total={total} />);
  expect(screen.getByText(/Jami/)).toHaveTextContent("75000");
});

test("haqiqiy calcTotal bilan", () => {
  render(<CartSummary items={items} />);
  expect(screen.getByText(/Jami/)).toHaveTextContent("75000");
});

Konsolda:

text
  ✓ soxta calcTotal bilan
  × haqiqiy calcTotal bilan
    → expected <p> to have text content '75000'
      Received: 'Jami: 40000 so\'m'
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Birinchi test "komponent total() natijasini chiqaradimi?" degan savolga javob berdi — lekin bu savolni hech kim bermagan edi. Mijoz to'laydigan summani faqat ikkinchi test tekshirdi va xatoni topdi: 2 ta osh 35 000 emas, 70 000. Soxta qiymatni testning o'zi yozgan — u o'zini o'zi tasdiqladi.

Bu Vitest II dagi automock tuzog'ining kattaroq ko'rinishi. Sof funksiya tez va barqaror — uni soxtalashga hech qanday sabab yo'q.

Tekshirib ko'ring: OrderPage ichida fetch("/api/menu"), formatPrice() va useNavigate() bor. Uchalasidan qaysi birini soxtalaysiz?

Javob

Faqat fetch — va u ham funksiya sifatida emas, tarmoq darajasida (MSW): so'rov URL'i va tanasi ham tekshiriladi. formatPrice — sof funksiya, haqiqiysi. useNavigate — haqiqiy router (createMemoryRouter, RTL III): sahifa almashganini ekrandan tekshirasiz.

3. Test ma'lumotlari: fabrika

3.1 Muammo: har testda to'liq obyekt

17-qismdan beri testlarda shunday qatorlar bor: { id: 1, matn: "Non", bajarildi: false }. Har test faylida, har testda. Uch muammo:

  • Shovqin. Testga faqat matn muhim, lekin o'quvchi uchala maydonni o'qiydi va "bu id: 1 nega muhim?" deb o'ylaydi.
  • O'zgarish narxi. Modelga yangi majburiy maydon qo'shilsa — 50 ta test faylini tahrirlaysiz.
  • Tasodifiy to'qnashuv. Ikki testda qo'lda id: 1 — ikki vazifa bitta id bilan, ro'yxatda key takrorlanadi.

Test ma'lumotlari fabrikasi (test data factory) — sukut qiymatli obyekt yasaydigan funksiya. Test faqat o'zi uchun muhim maydonni aytadi:

jsx
import { afterEach, describe, expect, test } from "vitest";

let nextId = 1;
const resetFactory = () => {
  nextId = 1;
};

function dish(overrides = {}) {
  const id = overrides.id ?? nextId++;
  const base = { id, name: `Taom ${id}`, price: 30000, left: 10 };
  return { ...base, ...overrides };
}

function dishes(n, change = () => ({})) {
  return Array.from({ length: n }, (_, i) => dish(change(i)));
}

const available = (list) => list.filter((d) => d.left > 0);

afterEach(resetFactory);

describe("menyu", () => {
  test("tugaganlar ko'rinmaydi", () => {
    const list = [
      dish({ name: "Osh" }),
      dish({ name: "Manti", left: 0 }),
    ];
    console.log(list.map((d) => d.id));
    expect(available(list).map((d) => d.name)).toEqual(["Osh"]);
  });

  test("100 taom, har 4-si tugagan", () => {
    const list = dishes(100, (i) => ({ left: i % 4 === 0 ? 0 : 5 }));
    expect(available(list)).toHaveLength(75);
    console.log(list[0].id, list.at(-1));
  });
});

Konsolda:

text
(2) [1, 2]
  ✓ menyu > tugaganlar ko'rinmaydi
1 {id: 100, name: 'Taom 100', price: 30000, left: 5}
  ✓ menyu > 100 taom, har 4-si tugagan
Testlar: 2 o'tdi (jami 2)

Ikkinchi test ham id 1 dan boshladi — afterEach(resetFactory) hisoblagichni tikladi. Busiz id testlar tartibiga bog'liq bo'lardi va Nozik xatolar dagi "tartibga bog'liq test" qaytib kelardi.

3.2 faker va fishery

Ikki mashhur kutubxona (2026-10-20, npm):

  • @faker-js/faker 10.6.0 — "haqiqatga o'xshash" ism, manzil, narx. O'zbek lotin lokali ham bor (fakerUZ_UZ_latin).
  • fishery 2.4.0 (2025-12) — fabrika quruvchi: Factory.define, build, buildList, sequence (o'suvchi raqam), rewindSequence.

Biz ikkalasini alohida papkada ishga tushirib ko'rdik (Node 24, kanon emas):

js
import { faker } from "@faker-js/faker";
import { Factory } from "fishery";

console.log(faker.person.firstName(), faker.person.firstName());
faker.seed(42);
const price = () =>
  faker.commerce.price({ min: 5000, max: 40000, dec: 0 });
console.log(faker.person.firstName(), price());

const dishFactory = Factory.define(({ sequence }) => ({
  id: sequence, name: `Taom ${sequence}`, price: 30000,
}));
console.log(dishFactory.build(), dishFactory.build({ name: "Osh" }));
text
Rodger Zackery
Nikita 25953
{ id: 1, name: 'Taom 1', price: 30000 } { id: 2, name: 'Osh', price: 30000 }

Faylni ikkinchi marta ishga tushirganimizda birinchi qator boshqacha chiqdi (Yadira Reymundo), seed(42) dan keyingisi esa yana Nikita 25953. Mana tasodifiy ma'lumotning narxi: seed'siz har ishga tushirishda boshqa ma'lumot. Test bir marta yiqilsa, uni aynan takrorlab bo'lmaydi.

Kanon ikkalasini ham olmadi. Sabab izohda: model uch maydonli (id, matn, bajarildi), 20 qatorlik o'z fabrikasi yetadi, tasodifiy matn esa natijani takrorlanmas qiladi. Katta modelda (20 maydonli foydalanuvchi, ichma-ich buyurtma) fishery ning associations va transient parametrlari foydali bo'ladi. faker — ko'proq demo ma'lumot va Storybook uchun; testda — faqat seed bilan.

4. Flaky testlar: to'rt sabab

Sabab Belgisi Davosi
vaqt ertalab o'tadi, kechqurun yiqiladi setSystemTime, soxta taymerlar
tartib yolg'iz o'tadi, hammasi bilan yiqiladi har testga yangi holat, beforeEach
async kutish yuk ostida yiqiladi findBy, waitFor, expect.poll
tashqi dunyo tarmoq, shrift, animatsiya MSW, animations: "disabled"

4.1 Vaqt: "bugun" bombasi

«Bahor» yangiliklar paneli bugun qo'shilgan taomga «Yangi» belgisini qo'yadi. Test bugungi sanani yozib qo'ygan — bugun o'tadi. Ertaga nima bo'lishini setSystemTime bilan ko'ramiz:

jsx
import { afterEach, expect, test, vi } from "vitest";

function isNew(dish, now = new Date()) {
  return dish.addedOn === now.toISOString().slice(0, 10);
}

const lagman = { name: "Lag'mon", addedOn: "2026-11-02" };

afterEach(() => {
  vi.useRealTimers();
});

for (const day of ["2026-11-02T09:00:00Z", "2026-11-03T09:00:00Z"]) {
  test(`qo'shilgan kuni yangi (${day.slice(0, 10)})`, () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date(day));
    expect(isNew(lagman)).toBe(true);
  });
}

Konsolda:

text
  ✓ qo'shilgan kuni yangi (2026-11-02)
  × qo'shilgan kuni yangi (2026-11-03)
    → expected false to be true // Object.is equality
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Xato kodda emas — testda: u "hozir" ni boshqarmagan. To'g'ri test vaqtni o'zi o'rnatadi va ikki holatni ham sinaydi: qo'shilgan kuni true, ertasiga false. Yana bir yashirin bomba bor: toISOString() — UTC sana. Toshkentda 00:00–05:00 orasida u kechagi sanani beradi (Date va vaqt zonalari). Bu ham chegaraviy holat sifatida testga yoziladi.

Soxta soat keyingi testga o'tadi. Vitest soxta taymerlarni test tugaganda o'zi o'chirmaydi. Yuqoridagi afterEach(() => vi.useRealTimers()) shuning uchun kerak. U bo'lmasa nima bo'ladi? Birinchi test soatni yoqib qoldiradi, ikkinchisi haqiqiy setTimeout(r, 20) ni kutadi. Uzoq kutmaslik uchun ikkinchi testga sozlama obyekti bilan qisqa chegara beramiz — { timeout: 300 } (sukut — 5000 ms):

jsx
import { expect, test, vi } from "vitest";

test("1: soxta soat yoqiladi, o'chirilmaydi", () => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date("2026-11-02T09:00:00Z"));
  expect(new Date().getDate()).toBe(2);
});

test("2: haqiqiy vaqt kutiladi", { timeout: 300 }, async () => {
  await new Promise((r) => setTimeout(r, 20));
  expect(true).toBe(true);
});

Konsolda:

text
  ✓ 1: soxta soat yoqiladi, o'chirilmaydi
  × 2: haqiqiy vaqt kutiladi
    → Test timed out in 300ms.
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Haqiqiy Vitest 5.0.4 da (biz sinadik, chegara sukut bo'yicha) xuddi shu:

text
 ❯ t/taymer.test.js (2 tests | 1 failed) 5017ms
   ✓ 1: soxta soat yoqiladi, o'chirilmaydi 5ms
   × 2: haqiqiy vaqt kutiladi 5011ms

 FAIL  t/taymer.test.js > 2: haqiqiy vaqt kutiladi
Error: Test timed out in 5000ms.

20 ms lik kutish 5 soniyalik timeout'ga aylandi: soxta soatni hech kim yurgizmadi. Ikkinchi testni yolg'iz ishga tushirsangiz — o'tadi. Bu tartibga bog'liq flaky'ning eng ko'p uchraydigan turi.

4.2 Tartib: yashirin bog'liqlik

Ikki test bitta modul darajasidagi savatni o'zgartiradi. Ikkinchisi birinchiga jim tayanadi. Buni ko'rish uchun birinchisini o'tkazib yuboramiz:

jsx
import { describe, expect, test } from "vitest";

const cart = [];

describe("savat", () => {
  test.skip("osh qo'shiladi", () => {
    cart.push({ name: "Osh", price: 35000 });
    expect(cart).toHaveLength(1);
  });
  test("choy qo'shiladi", () => {
    cart.push({ name: "Ko'k choy", price: 5000 });
    expect(cart).toHaveLength(2);
  });
});

Konsolda:

text
  ↓ savat > osh qo'shiladi [skipped]
  × savat > choy qo'shiladi
    → expected [ …(1) ] to have a length of 2 but got 1
Testlar: 1 yiqildi, 0 o'tdi, 1 o'tkazildi (jami 2)

[ …(1) ] — Vitest massivni xabarda qisqartiradi: "bitta elementli massiv". Ichida nima borligini ko'rish kerak bo'lsa — toEqual bilan tekshiring, u butun qiymatni chiqaradi.

skip siz ikkalasi ham o'tadi — shuning uchun xato ko'rinmay yuradi. Lekin kimdir .only yozsa, tartib o'zgarsa yoki Vitest testlarni aralashtirsa — qizil. Haqiqiy Vitest'da tartibni ataylab aralashtirish mumkin: --sequence.shuffle. Tartib seed (urug') bilan belgilanadi — K-10 dagi --test-random-seed ning Vitest'dagi o'xshashi. Uch testli savat (osh, choy, jami) bilan biz seed 1–6 ni sinadik: 1, 4, 5, 6 — o'tdi, 2 va 3 — yiqildi. Seed 2:

text
 RUN  v5.0.4 C:/Users/…/sinov
      Running tests with seed "2"

 ❯ t/tartib.test.js (3 tests | 2 failed) 10ms
   ❯ savat (3)
     × osh qo'shiladi 1ms
     × choy qo'shiladi 8ms
     ✓ jami 0ms

Yiqilgan seed'ni yozib oling: npx vitest run --sequence.shuffle --sequence.seed=2 xatoni har safar takrorlaydi. Davosi — har test o'z holatini yaratsin: let cart; beforeEach(() => { cart = []; }), yoki fabrika.

4.3 Async: "darhol" va "tugagach"

Bu kanonda topilgan ikkinchi flaky testning sodda modeli. Vazifa qo'shilganda qator darhol chiqadi (optimistik yangilanish, useOptimistic). Maydonni esa forma serverga yozish tugagach tozalaydi. Odatda ikkalasi deyarli bir vaqtda — test sinxron tekshiradi va o'tadi. Bu yerda saqlash 50 ms davom etadi:

jsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test } from "vitest";

const save = () => new Promise((r) => setTimeout(r, 50));

function AddTask() {
  const [tasks, setTasks] = useState([]);
  const [text, setText] = useState("");
  async function submit(e) {
    e.preventDefault();
    setTasks((t) => [...t, text]);
    await save();
    setText("");
  }
  return (
    <form onSubmit={submit}>
      <input
        aria-label="Yangi vazifa"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <ul>
        {tasks.map((t) => (
          <li key={t}>{t}</li>
        ))}
      </ul>
    </form>
  );
}

test("sinxron tekshiruv", async () => {
  const user = userEvent.setup();
  render(<AddTask />);
  const field = screen.getByRole("textbox", { name: "Yangi vazifa" });
  await user.type(field, "Non olish{Enter}");
  expect(screen.getByRole("listitem")).toHaveTextContent("Non olish");
  expect(field).toHaveValue("");
});

test("waitFor bilan", async () => {
  const user = userEvent.setup();
  render(<AddTask />);
  const field = screen.getByRole("textbox", { name: "Yangi vazifa" });
  await user.type(field, "Non olish{Enter}");
  expect(screen.getByRole("listitem")).toHaveTextContent("Non olish");
  await waitFor(() => expect(field).toHaveValue(""));
});

Konsolda:

text
  × sinxron tekshiruv
    → expected <input> to have value '', received 'Non olish'
  ✓ waitFor bilan
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Qator darhol bor — getByRole to'g'ri. Maydon esa hali to'la. waitFor tekshiruvni har 50 ms qayta urinadi va sukut bo'yicha 1 s gacha kutadi (RTL II). Qoida: keyinroq bo'ladigan narsani findBy yoki waitFor bilan tekshiring, hatto "odatda darhol" bo'lsa ham. "Odatda" — flaky'ning boshqa nomi.

4.4 Async: kutish chegarasi

findBy sukut bo'yicha 1 s kutadi. Element 1.2 s da kelsa-chi?

jsx
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { expect, test } from "vitest";

function SlowMenu() {
  const [ready, setReady] = useState(false);
  useEffect(() => {
    const id = setTimeout(() => setReady(true), 1200);
    return () => clearTimeout(id);
  }, []);
  return ready ? <h2>Menyu</h2> : <p>Yuklanmoqda…</p>;
}

test("sukut — 1 s", async () => {
  render(<SlowMenu />);
  await expect(screen.findByRole("heading")).rejects.toThrow(
    "Unable to find role",
  );
});

test("timeout — 3 s", async () => {
  render(<SlowMenu />);
  const h = await screen.findByRole("heading", {}, { timeout: 3000 });
  console.log(h.textContent);
});

Konsolda:

text
  ✓ sukut — 1 s
Menyu
  ✓ timeout — 3 s
Testlar: 2 o'tdi (jami 2)

Kanondagi birinchi flaky aynan shunday edi — faqat 1.2 s ni yuk "yasagan". Bu yerdan xulosa chiqarish oson: "hamma joyda timeout'ni 10 s qilaylik". Bu xato yo'l. Haqiqiy xatoda test baribir yiqiladi — faqat 10 s kechroq, va 300 test × 10 s = to'plam soatlab ishlaydi. Chegarani faqat o'lchangan sabab bilan oshiring va sababni izohda yozing (pastdagi «Vazifalar qadami»).

Tekshirib ko'ring: Test lokalda doim o'tadi, CI'da 10 dan 1 marta yiqiladi: "Unable to find role heading". Birinchi qadamingiz qanday?

Javob

Xatoni lokalda takrorlash: yuk berib (ikkinchi test jarayoni fonda) bir necha marta ishga tushirish, --sequence.shuffle bilan tartibni aralashtirish. Keyin sabab: element qancha vaqtda chiqyapti, test nimani kutyapti. Faqat shundan keyin tuzatish. "CI'da retry: 3" — sababni yashiradi, tuzatmaydi.

5. Flaky'ni topish, yashirmaslik

5.1 Topish usullari

Vosita Buyruq Nima qiladi
Vitest --sequence.shuffle --sequence.seed=N tartibga bog'liqlikni ochadi
Vitest fonda ikkinchi vitest (yuk) sekin mashina, sekin CI'ni taqlid qiladi
Playwright --repeat-each=10 har testni 10 marta
Playwright retries: 2 (CI) 2-urinishda o'tgani — hisobotda flaky

Playwright'ning retries hisoboti foydali: test yashil, lekin "flaky" degan sariq belgi bilan — muammo ko'rinib turadi. Vitest'da ham qayta urinish bor: test("…", { retry: 2 }, fn). Biz sinadik — birinchi urinishda yiqilgan test terminalda shunday chiqdi:

text
 ✓ t/retry.test.js > ba'zan yiqiladi 8ms (retry x1)

Yashil belgi va qavs ichida kichik izoh. Kim buni har kuni o'qiydi? Natija oynasida ham ko'ring — test faqat ikkinchi urinishda o'tadi:

jsx
import { expect, test } from "vitest";

let attempts = 0;

test("ba'zan yiqiladi", { retry: 2 }, () => {
  attempts += 1;
  console.log(`urinish: ${attempts}`);
  expect(attempts).toBe(2);
});

Konsolda:

text
urinish: 1
urinish: 2
  ✓ ba'zan yiqiladi
Testlar: 1 o'tdi (jami 1)

Birinchi urinish yiqildi, lekin hisobotda faqat yashil ✓ qoldi. retry — vaqtinchalik yordam, tuzatish emas: sabab o'z joyida turibdi.

5.2 Karantin

Ba'zan sababni darhol topib bo'lmaydi, lekin test butun jamoani to'xtatyapti. Shunda karantin: test asosiy to'plamdan chiqariladi, lekin yo'qolmaydi — alohida ishga tushadi va muddati bor. Kanondagi tartib (TESTLAR.md):

text
3. **Karantin** (tuzatish darhol bo'lmasa): testga teg —
   `test("...", { tag: "@karantin" }, ...)` va izohda issue havolasi + sana.
   `npm run test:e2e` uni o'tkazib yuboradi (`--grep-invert @karantin`),
   `npm run test:karantin` alohida, 3 urinish bilan ishga tushiradi.
   Qoida: karantinda **14 kundan ortiq** test qolmaydi — tuzatiladi yoki
   o'chiriladi (test kodni himoya qilmasa, u faqat shovqin).

Karantindagi testning yo'li — ikki chiqish bilan, uchinchisi yo'q ("abadiy karantin"):

flowchart TD
  A["Test ba'zan yiqiladi"] --> B{"Sabab darhol topildimi?"}
  B -- "ha" --> C["Tuzatish + yuk ostida 5/5"]
  B -- "yo'q" --> D["Karantin: @karantin tegi, issue, sana"]
  D --> E["test:karantin — alohida, 3 urinish"]
  E --> F{"14 kun ichida tuzatildimi?"}
  F -- "ha" --> G["Teg olinadi — asosiy to'plamga qaytadi"]
  F -- "yo'q" --> H["Test o'chiriladi"]
  C --> G

Hozir karantinda 0 test — --pass-with-no-tests shu uchun: bo'sh to'plam xato emas.

6. Tezlik, CI va coverage

6.1 Tezlik: avval o'lchang

Sekin to'plamni kam ishga tushirishadi. Kanon eng sekin testlarni JSON reporter bilan o'lchadi (348 test): 1 s dan sekin — 1 ta, 500 ms dan sekin — 8 ta. Eng sekini — harakatlar "vazifa qo'shish", 1 480 ms: user.type har harfni alohida hodisalar zanjiri bilan yozadi. Fayllar bo'yicha: App 2.5 s, harakatlar 2.4 s, Menyu 1.7 s.

Butun npm test — 6.1 s (issiq). Yangi paketdan keyingi birinchi ishga tushirish esa 44–110 s: Vite keshi qayta yig'iladi. Xulosa: hozir tezlashtirishga sabab yo'q. Testni "tezlashtirish uchun" soxtalash (masalan, user.type o'rniga qiymatni to'g'ridan qo'yish) — foydalanuvchi xulqidan uzoqlashish.

6.2 CI

Playwright II da yozilgan .github/workflows/test.yml da uch job parallel: unit (Vitest + coverage chegarasi, hisobot artifact), brauzer (Browser Mode + Storybook), e2e (Playwright, CI'da retries: 2, trace: "on-first-retry", forbidOnly). Vizual testlar CI'da yo'q — oltin rasmlar faqat Windows'da (Visual regression). Halol eslatma: fayl actionlint bilan tekshirilgan (0 xato), lekin GitHub'ga push qilinmagan va Actions'da ishlamagan.

Strategiya bo'yicha: tez qavat har push'da, sekin qavat PR'da; forbidOnly — qolib ketgan test.only CI'ni yiqitadi (Vitest I dagi tuzoq).

6.3 Coverage mifi

vazifalar qamrovi — satr 62.72 %. Bu qismda testlar topgan haqiqiy xatolarni eslang:

Xato Kim topdi
«Qayta urinish» tugmasi doim ko'rinadi vizual test (FT-13)
"server rad etdi" testi tasodifan o'tardi MSW, haqiqiy kechikish (FT-07)
<li> <ul> siz (story) Storybook a11y (FT-12)
form reset sinxron tekshirilgan yuk ostida ishga tushirish (shu dars)
birinchi test 1 s dan oshadi yuk ostida ishga tushirish (shu dars)
test:tarmoq hech qachon ishlamagan testlar xaritasi (FT-01)

Ularning hech birini coverage raqami ko'rsatmagan. Coverage "bu kod umuman ishladimi?" degan savolga javob beradi (Vitest II). "Test to'g'ri narsani tekshiradimi?" — boshqa savol. Chegara (60 %) — pastga tushmaslik uchun. 100 % maqsad qilinsa, ma'nosiz testlar paydo bo'ladi.

7. Vazifalar qadami: fabrika, flaky tuzatishlar, karantin (holat 15)

7.1 Flaky — topildi

bash
git switch -c test/strategiya

Usul: npm run test:qamrov va fonda yana bitta vitest --project komponent. Tuzatishdan oldin 3 marta:

Urinish Natija Vaqt
1 348/348 15.2 s
2 2 yiqildi 29.4 s
3 348/348 26.4 s

Yiqilganlar: VazifaSahifasi "loader serverdan oladi" (sarlavha topilmadi, 1 615 ms) va VazifaTahriri "bo'sh matn" (Matn label topilmadi, 1 691 ms). Ikkalasi ham faylning birinchi testi: sovuq import, birinchi MSW so'rovi va router — hammasi shu testga tushadi. findBy* esa 1 000 ms kutadi.

7.2 Tuzatish 1: o'lchangan chegara

ts
// 22/#15: findBy*/waitFor kutishi 1 s (sukut) → 3 s.
// O'lchandi: parallel yuk ostida (ikkinchi vitest fonda)
// faylning BIRINCHI testi — sovuq import, birinchi MSW
// so'rovi, router — 1.6–1.7 s oldi va 3 urinishdan birida
// 2 test yiqildi (VazifaSahifasi, VazifaTahriri). Xato rost
// bo'lsa test baribir yiqiladi — faqat 3 s kechroq
configure({ asyncUtilTimeout: 3000 });

configure — @testing-library/react dan, vitest-sozlash.ts da. Izoh to'rt narsani aytadi: nima o'zgardi, nega (o'lchov raqamlari bilan), qaysi testlar, va narxi. "Kutishni oshirmang" qoidasidan istisno — chunki sabab o'lchangan va chegara 3 s, 30 emas.

7.3 Tuzatish 2: form reset

Keyingi yuk sinovida 5 dan 1 tasida yangi test yiqildi: harakatlar "vazifa qo'shish". Bu «Async: "darhol" va "tugagach"» bo'limidagi holatning o'zi:

ts
    // 22/#15 (beqaror edi): qator optimistik — DARHOL;
    // maydonni esa React action TUGAGACH tozalaydi (form
    // reset). Odatda bir vaqtda, lekin yuk ostida reset
    // kechroq — sinxron tekshiruv 5 tadan 1 da yiqildi
    await waitFor(() => expect(maydon).toHaveValue(""));

Natija: o'zaro yuk ostida 10/10 (komponent 87/87, qamrov 348/348). Playwright: --repeat-each=10 → 201/201, flaky 0, 150 s. RTL II da ko'rgan "Test timed out in 5000ms" bu safar takrorlanmadi — u o'shanda kompyuterdagi tashqi yukdan edi.

7.4 Fabrika

tekshiruv/yordamchi/fabrika.ts — «Test ma'lumotlari: fabrika» bo'limidagi g'oyaning TypeScript'dagi ko'rinishi:

ts
let keyingiId = 1;

/** Hisoblagich — testlar orasida (vitest-sozlash.ts, afterEach) */
export function fabrikaniTikla(): void {
  keyingiId = 1;
}

export function vazifa(
  ozgarish: Partial<VazifaMalumoti> = {},
): VazifaMalumoti {
  const id = ozgarish.id ?? keyingiId++;
  return { id, matn: `Vazifa ${id}`, bajarildi: false, ...ozgarish };
}

vazifalar(n, (i) => ...) — n ta vazifa. Partial<VazifaMalumoti> — hamma maydon ixtiyoriy (Utility types), lekin faqat modelda bor maydonlar: xato nomli maydonni TypeScript ushlaydi. Ishlatilishi: serverRoyxati([vazifa({ matn: "Non" })]). Yangi test — 250 vazifa, har 3-si bajarilgan:

ts
    serverRoyxati(vazifalar(250, (i) => ({ bajarildi: i % 3 === 0 })));
    const { vazifalar: kelgan } = await api.royxat();
    expect(kelgan).toHaveLength(250);
    expect(kelgan.filter((v) => v.bajarildi)).toHaveLength(84);
    expect(kelgan.at(-1)).toEqual({
      id: 250,
      matn: "Vazifa 250",
      bajarildi: true, // i = 249 — 3 ga bo'linadi
    });

Halol tafsilot: birinchi yozilishda kutilgan qiymat noto'g'ri edi — oxirgi vazifa bajarildi: false deb yozilgan. Test to'g'ri yiqildi: 249 3 ga bo'linadi. Kutilgan qiymat tuzatildi. Test ma'lumotini hisoblab yozish ham xato qilishi mumkin — shuning uchun test birinchi marta yiqilganda avval testni o'qing.

7.5 Karantin va TESTLAR.md

package.json:

json
"test:e2e": "playwright test --project=chrome --grep-invert @karantin",
"test:karantin": "playwright test --project=chrome --grep @karantin --retries=3 --pass-with-no-tests"

TESTLAR.md qaytarildi: holat 01 dagi fayl holat 02 dan beri yo'qolgan edi. Endi unda to'rt bo'lim bor: qavatlar jadvali, test ma'lumotlari, flaky tartibi va ma'lum cheklovlar. Flaky tartibi uch qadam: topish, sababni tuzatish, kerak bo'lsa karantin. TEXNIK-QARZ.md da 22-qism holati yangilandi va 71–74 qatorlar qo'shildi (Linux suratlari, axe-core ikki nusxa, Storybook skipLibCheck, Browser Mode coverage).

7.6 Natija va commit

Testlar: Vitest 349, brauzer 6, Storybook 12, e2e 21, vizual 6. npm test 6.1 s; qamrov satr 62.72 % (o'zgarmadi). JS 206.7 KB — o'zgarmadi; yangi paket yo'q.

bash
git add -A
git commit

Commit (kanon): test: fabrika, flaky tuzatishlari (asyncUtilTimeout, form reset), karantin qoidasi; TESTLAR.md.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
soxta bilan o'tadi, ilovada xato o'z mantiqingiz soxtalangan faqat chegarani soxtalang
ertaga yiqiladi "hozir" boshqarilmagan setSystemTime
yolg'iz o'tadi, hammasi bilan yiqiladi umumiy holat yoki soxta soat oqdi beforeEach, afterEach(useRealTimers)
yuk ostida yiqiladi sinxron tekshiruv findBy, waitFor
timeout hamma joyda 10 s sabab o'lchanmagan o'lchang, izoh bilan oshiring
CI'da retry: 3, hamma xursand flaky yashirildi sabab + karantin muddati
faker bilan yiqilgan testni takrorlab bo'lmaydi seed yo'q faker.seed(N) yoki fabrika
"coverage 90 %, demak xato yo'q" coverage — sifat emas xatolar jadvaliga qarang

9. Mashqlar

Mashqlar kurs/mashqlar/22/15-strategiya-flaky/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Tartibni aralashtirish: npx vitest run --sequence.
  • "Hozir" ni o'rnatish: vi.(new Date("2026-11-02"))
  • Keyin bo'ladigan narsani kutish: await (() => expect(x).toHaveValue(""))
  • Testing Library'ning global kutish chegarasi: configure({ : 3000 })
  • Playwright'da har testni 10 marta: --=10
Yechim
  • shuffle — seed chiqishda yoziladi; --sequence.seed=N bilan takrorlanadi.
  • setSystemTime — vi.useFakeTimers() bilan; new Date() va Date.now() soxta vaqtni beradi.
  • waitFor — har 50 ms qayta urinadi, sukut 1 s.
  • asyncUtilTimeout — findBy* va waitFor uchun; faqat o'lchangan sabab bilan.
  • repeat-each — flaky'ni ochish uchun; retries esa yashiradi.

2-mashq (o'rta): tartibga bog'liq testlarni tuzating

Uch test bitta orders massivini o'zgartiradi: (1) buyurtma qo'shiladi — uzunligi 1; (2) yana biri — uzunligi 2; (3) jami summa 63 000. Ular faqat shu tartibda o'tadi. Qayta yozing: fabrika order({ ... }) (o'suvchi id, sukut: dish: "Osh", price: 35000), har test o'z massivi bilan, hisoblagich afterEach da tiklanadi. Uchinchi testni test.only bilan yolg'iz ishga tushirsa ham o'tsin.

Ishora: jami uchun orders ni test ichida yarating: order() va order({ dish: "Lag'mon", price: 28000 }).

Yechim
jsx
import { afterEach, describe, expect, test } from "vitest";

let nextId = 1;

function order(overrides = {}) {
  const id = overrides.id ?? nextId++;
  return { id, dish: "Osh", price: 35000, ...overrides };
}

const total = (list) => list.reduce((s, o) => s + o.price, 0);

afterEach(() => {
  nextId = 1;
});

describe("buyurtmalar", () => {
  test("bitta buyurtma", () => {
    const orders = [order()];
    expect(orders).toHaveLength(1);
    expect(orders[0].id).toBe(1);
  });

  test("ikki buyurtma — id'lar har xil", () => {
    const orders = [order(), order()];
    console.log(orders.map((o) => o.id));
    expect(orders).toHaveLength(2);
  });

  test("jami summa", () => {
    const lagman = order({ dish: "Lag'mon", price: 28000 });
    const orders = [order(), lagman];
    expect(total(orders)).toBe(63000);
  });
});

Konsolda:

text
  ✓ buyurtmalar > bitta buyurtma
(2) [1, 2]
  ✓ buyurtmalar > ikki buyurtma — id'lar har xil
  ✓ buyurtmalar > jami summa
Testlar: 3 o'tdi (jami 3)

Endi har test o'z ma'lumotini o'zi yaratadi — umumiy holat yo'q. Ikkinchi testda id'lar 1 va 2: hisoblagich birinchi testdan keyin tiklandi. Istalgan testni yolg'iz, istalgan tartibda ishga tushiring — natija bir xil.

3-mashq (qiyin): aksiya banneri — vaqt va async

PromoBanner «Bahor» aksiyasini ko'rsatadi: tugash sanasi endsAt ("2026-11-10T19:00:00Z", Toshkentda 11-noyabr 00:00). Qolgan kunlarni Math.ceil bilan hisoblaydi: «Aksiya tugashiga N kun», oxirgi kuni «Aksiya bugun tugaydi», o'tib ketsa — «Aksiyalar yo'q». Ma'lumot loadPromo() dan keladi (Promise, 100 ms). Testlar: 7-noyabr 10:00 da — necha kun chiqishini avval qo'lda hisoblang, keyin testga yozing; 10-noyabr 23:00 (Toshkent) da — «bugun tugaydi»; 11-noyabr 01:00 da — status yo'q. Vaqtni setSystemTime bilan bering, banner kelishini findBy bilan kuting.

Ishora: findBy ishlashi uchun setTimeout haqiqiy qolishi kerak (Vitest II dagi "soxta soatni kech yoqing" tuzog'i). Ikki yo'l bor: vi.setSystemTime ni vi.useFakeTimers() siz chaqirish (Vitest shunda faqat Date ni soxtalaydi) yoki buni aniq yozish — vi.useFakeTimers({ toFake: ["Date"] }). Tiklash — vi.useRealTimers(). "Banner yo'q" ni tekshirishdan oldin yuklanish tugaganini kuting.

Yechim
jsx
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { afterEach, expect, test, vi } from "vitest";

const DAY = 24 * 60 * 60 * 1000;

function daysLeft(endsAt, now = Date.now()) {
  return Math.ceil((new Date(endsAt).getTime() - now) / DAY);
}

function PromoBanner({ loadPromo }) {
  const [promo, setPromo] = useState(null);
  useEffect(() => {
    loadPromo().then(setPromo);
  }, [loadPromo]);
  if (!promo) return <p>Yuklanmoqda…</p>;
  const left = daysLeft(promo.endsAt);
  if (left <= 0) return <p>Aksiyalar yo'q</p>;
  const text = left === 1 ? "bugun tugaydi" : `tugashiga ${left} kun`;
  return <p role="status">Aksiya {text}</p>;
}

const loadPromo = () =>
  new Promise((r) => {
    setTimeout(() => r({ endsAt: "2026-11-10T19:00:00Z" }), 100);
  });

afterEach(() => {
  vi.useRealTimers();
});

const cases = [
  ["7-noyabr 10:00", "2026-11-07T05:00:00Z", "tugashiga 4 kun"],
  ["10-noyabr 23:00", "2026-11-10T18:00:00Z", "bugun tugaydi"],
];

for (const [name, now, text] of cases) {
  test(name, async () => {
    vi.setSystemTime(new Date(now));
    render(<PromoBanner loadPromo={loadPromo} />);
    expect(await screen.findByRole("status")).toHaveTextContent(text);
  });
}

test("11-noyabr 01:00 — banner yo'q", async () => {
  vi.setSystemTime(new Date("2026-11-10T20:00:00Z"));
  render(<PromoBanner loadPromo={loadPromo} />);
  await screen.findByText("Aksiyalar yo'q");
  expect(screen.queryByRole("status")).toBeNull();
});

Konsolda:

text
  ✓ 7-noyabr 10:00
  ✓ 10-noyabr 23:00
  ✓ 11-noyabr 01:00 — banner yo'q
Testlar: 3 o'tdi (jami 3)

Qo'lda hisoblaganingiz 3 chiqqan bo'lsa — test sizni tuzatdi: 3 kun 14 soat qolgan, Math.ceil esa yuqoriga yaxlitlaydi, natija 4 kun. Bu talab masalasi — "qolgan to'liq kunlar" desak Math.floor va oxirgi kun mantig'i boshqacha bo'lardi. Test shunday savollarni ochiq qiladi; javobni talab egasi beradi, test esa uni yozib qo'yadi.

Uchinchi testda findByText("Aksiyalar yo'q") muhim: usiz queryByRole("status") yuklanish paytida tekshirilib, har doim null berardi — test hech narsani isbotlamasdi.

4-mashq: Vazifalar qadami

test/strategiya branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: fabrika.ts va uning afterEach dagi tiklanishi, configure({ asyncUtilTimeout: 3000 }) (izohi bilan), harakatlar dagi waitFor, karantin skriptlari, TESTLAR.md. Avval flaky'ni o'zingiz ko'ring.

Yechim

Tekshiruv ro'yxati:

  • Tuzatishlardan oldin: bir terminalda npx vitest --project komponent, ikkinchisida npm run test:qamrov — 3–5 marta. Yiqilsa, test nomi va vaqtini yozing (kompyuteringiz tezroq bo'lsa, yiqilmasligi ham mumkin — bu ham natija).
  • Tuzatishlardan keyin o'sha usul bilan 5/5.
  • npx vitest run --sequence.shuffle — 2–3 xil seed bilan hammasi o'tadi.
  • npm run test:karantin — 0 test, chiqish kodi 0.
  • npm test — 349 test; npm run build — JS 206.7 KB.

10. Real ishda

  • Katta jamoalarda flaky testlar alohida hisobotda kuzatiladi: qaysi test qancha marta "2-urinishda o'tdi". Google, Microsoft kabi kompaniyalar o'z flaky statistikasini ochiq maqolalarda yozgan — muammo hamma joyda bor.
  • Kod ko'rib chiqishda: sleep, katta timeout, retry, toBeCloseTo bilan vaqt — "bu yerda nimani kutyapmiz?" degan savol.
  • Test ma'lumotlari backend'da ham — bazani to'ldirish (seeding) va fabrikalar (Seeding va test ma'lumotlari).
  • Intervyu savollari: "flaky testni qanday topasiz va tuzatasiz?", "nimani mock qilasiz?", "coverage 100 % bo'lsa xato yo'qmi?", "test ma'lumotini qanday yaratasiz?".

Xulosa

  • Faqat chegarani soxtalang (tarmoq, vaqt, tasodif, yo'q API); o'z mantiqingizni soxtalasangiz, test o'zini o'zi tasdiqlaydi.
  • Fabrika — faqat muhim maydon, mantiqiy sukut, o'suvchi id va har testdan keyin tiklash; faker — faqat seed bilan.
  • Flaky'ning to'rt sababi: vaqt, tartib, async kutish, tashqi dunyo — har biriga aniq davo bor.
  • Flaky'ni yuk, --sequence.shuffle va --repeat-each bilan oching; retry yashiradi, karantin — muddatli.
  • Coverage — tekshirilmagan kod xaritasi; qismdagi 6 haqiqiy xatoning hech birini u topmagan.
  • vazifalar: yuk ostida 3 dan 1 → 10/10, Playwright 201/201; fabrika, karantin, TESTLAR.md; 349 test.

Keyingi dars: Jest va eski test muhitlari — Jest'li loyihani o'qish, jest.config, ts-jest va babel-jest, CRA testlari, Enzyme va Jest'dan Vitest'ga ko'chish.

Manbalar

  • Vitest: sequence.shuffle, sequence.seed, retry (vitest.dev/config), vi.useFakeTimers, vi.setSystemTime (vitest.dev/api/vi)
  • Testing Library: configure({ asyncUtilTimeout }), Async methods — waitFor, findBy (testing-library.com/docs/dom-testing-library/api-async)
  • Playwright: Test retries — flaky testlar (playwright.dev/docs/test-retries), --repeat-each, tags va --grep-invert
  • fishery (github.com/thoughtbot/fishery), @faker-js/faker — seed, lokallar (fakerjs.dev)
  • Martin Fowler, "Eradicating Non-Determinism in Tests" (martinfowler.com)
  • Haqiqiy chiqishlar: Vitest 5.0.4, Node 24.21.0, @faker-js/faker 10.6.0, fishery 2.4.0, Windows 11 (2026-10-20)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Test strategiyasi: nimani soxtalash, test ma'lumotlari fabrikasi va flaky testlar — IlmHamroh