Mundarija (36)
- Bu darsda
- 1. Nega bu kerak?
- 2. Nimani soxtalash kerak
- 2.1 Qoida: chegarada soxtalang
- 2.2 O'z mantiqingizni soxtalasangiz
- 3. Test ma'lumotlari: fabrika
- 3.1 Muammo: har testda to'liq obyekt
- 3.2 faker va fishery
- 4. Flaky testlar: to'rt sabab
- 4.1 Vaqt: "bugun" bombasi
- 4.2 Tartib: yashirin bog'liqlik
- 4.3 Async: "darhol" va "tugagach"
- 4.4 Async: kutish chegarasi
- 5. Flaky'ni topish, yashirmaslik
- 5.1 Topish usullari
- 5.2 Karantin
- 6. Tezlik, CI va coverage
- 6.1 Tezlik: avval o'lchang
- 6.2 CI
- 6.3 Coverage mifi
- 7. Vazifalar qadami: fabrika, flaky tuzatishlar, karantin (holat 15)
- 7.1 Flaky — topildi
- 7.2 Tuzatish 1: o'lchangan chegara
- 7.3 Tuzatish 2: form reset
- 7.4 Fabrika
- 7.5 Karantin va TESTLAR.md
- 7.6 Natija va commit
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): tartibga bog'liq testlarni tuzating
- 3-mashq (qiyin): aksiya banneri — vaqt va async
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
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'rnigafindBy/waitFor, "hozir" o'rnigasetSystemTime, 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/fakervafisheryqachon 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.
- 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:
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:
✓ 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:
OrderPageichidafetch("/api/menu"),formatPrice()vauseNavigate()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
matnmuhim, lekin o'quvchi uchala maydonni o'qiydi va "buid: 1nega 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'yxatdakeytakrorlanadi.
Test ma'lumotlari fabrikasi (test data factory) — sukut qiymatli obyekt yasaydigan funksiya. Test faqat o'zi uchun muhim maydonni aytadi:
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:
(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/faker10.6.0 — "haqiqatga o'xshash" ism, manzil, narx. O'zbek lotin lokali ham bor (fakerUZ_UZ_latin).fishery2.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):
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" }));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:
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:
✓ 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):
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:
✓ 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:
❯ 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:
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:
↓ 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:
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 0msYiqilgan 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:
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:
× 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?
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:
✓ 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:
✓ 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:
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:
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):
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 --> GHozir 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
git switch -c test/strategiyaUsul: 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
// 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:
// 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:
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:
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:
"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.
git add -A
git commitCommit (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=Nbilan takrorlanadi. - setSystemTime —
vi.useFakeTimers()bilan;new Date()vaDate.now()soxta vaqtni beradi. - waitFor — har 50 ms qayta urinadi, sukut 1 s.
- asyncUtilTimeout —
findBy*vawaitForuchun; faqat o'lchangan sabab bilan. - repeat-each — flaky'ni ochish uchun;
retriesesa 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
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:
✓ 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
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:
✓ 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, ikkinchisidanpm 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,toBeCloseTobilan 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
seedbilan. - Flaky'ning to'rt sababi: vaqt, tartib, async kutish, tashqi dunyo — har biriga aniq davo bor.
- Flaky'ni yuk,
--sequence.shuffleva--repeat-eachbilan oching;retryyashiradi, 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)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!