Mundarija (36)
- Bu darsda
- 1. Nega bu kerak?
- 2. G'oya: so'rovni manzil bo'yicha moslash
- 2.1 Kichik o'quv modeli
- 2.2 MSW qayerda ushlaydi
- 3. Handler yozish
- 3.1 http va HttpResponse
- 3.2 Tana va parametrlar
- 4. setupServer: Vitest'da ulash
- 4.1 Hayot sikli
- 4.2 onUnhandledFrame: handler yo'q so'rov
- 5. Komponent testi MSW bilan
- 5.1 Asosiy oqim va xato holatlari
- 5.2 So'rovni tekshirish kerakmi?
- 6. Brauzerda: setupWorker
- 6.1 mockServiceWorker.js
- 6.2 Qayerda ishlatiladi
- 7. Natija oynasida nega yo'q
- 8. Tuzoq: listen() kech qoldi
- 9. Vazifalar qadami: MSW (holat 07)
- 9.1 O'rnatish va narx
- 9.2 tekshiruv/msw/soxta-api.ts
- 9.3 vitest-sozlash.ts
- 9.4 Ko'chirish: 155 qator ketdi, 91 keldi
- 9.5 msw.test.tsx (5 test) — qo'lda soxta ko'rmagan narsalar
- 9.6 Biz topgan xato: bo'sh alert
- 9.7 Natija va commit
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): o'quv modelida parametr va xato
- 3-mashq (qiyin): o'z loyihangizda MSW
- 4-mashq: Vazifalar qadami
- 12. Real ishda
- Xulosa
- Manbalar
MSW: API'ni tarmoq darajasida mock qilish — handler, setupServer va xato holatlari
Qisqacha: Qo'lda yozilgan
fetchsoxtasi (vi.fn(async () => Response.json(…))) har qanday so'rovga javob beradi — URL'da xato bo'lsa ham test o'tadi. MSW (Mock Service Worker) so'rovni tarmoq darajasida ushlaydi: ilova haqiqiyfetchqiladi, MSW esa uni manzil va metod bo'yicha handlerga moslaydi (http.get("/api/menu", () => HttpResponse.json([…]))). Testda —setupServer(msw/node), brauzerda —setupWorker(msw/browser), handlerlar bir xil. Bitta test uchun boshqa xulq —server.use(…)(500,HttpResponse.error(),delay()), keyinresetHandlers(). Handler yo'q so'rov —onUnhandledFrame: "error"bilan xato.
Bu darsda
- Qo'lda
fetchsoxtasi nimani ko'rmasligini va MSW buni qanday ushlashini ko'rasiz. http.get/post/patch/delete,HttpResponse, URL parametrlari bilan handler yozasiz.setupServer,listen/resetHandlers/closevaonUnhandledFrameni sozlaysiz.server.usebilan 500, tarmoq xatosi va sekin javobni sinaysiz.setupWorker,mockServiceWorker.jsva kanon topgan ikki tuzoqni bilasiz.
Oldin bilishingiz kerak: RTL III, API qatlami va type-safe kontrakt, Vitest II, Service Worker.
1. Nega bu kerak?
RTL II dagi vazifalar testlarida soxta server shunday edi — fetch ning vi.fn almashtirmasi, metod bo'yicha if, har faylda 20–26 qator. Nega test yozamiz darsidagi testlar xaritasida bu bo'shliq yozilgan: "fetch qo'lda soxtalanadi — URL/metod xatosi ushlanmaydi". Nega ushlanmaydi — jonli ko'ramiz.
Sardor menyu komponentini yozdi. Server manzili /api/menu, Sardor esa /api/menyu deb yozib qo'ydi:
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { afterEach, expect, test, vi } from "vitest";
function Menu() {
const [dishes, setDishes] = useState(null);
useEffect(() => {
fetch("/api/menyu") // server: /api/menu — xato!
.then((r) => r.json())
.then(setDishes);
}, []);
if (!dishes) return <p role="status">Yuklanmoqda…</p>;
return (
<ul aria-label="Menyu">
{dishes.map((d) => <li key={d.id}>{d.name}</li>)}
</ul>
);
}
afterEach(() => vi.unstubAllGlobals());
test("qo'lda soxta fetch — test o'tadi", async () => {
const fakeFetch = vi.fn(async () =>
Response.json([{ id: 1, name: "Osh" }]),
);
vi.stubGlobal("fetch", fakeFetch);
render(<Menu />);
expect(await screen.findByText("Osh")).toBeInTheDocument();
console.log("so'ralgan:", fakeFetch.mock.calls[0][0]);
});Konsolda:
so'ralgan: /api/menyu
✓ qo'lda soxta fetch — test o'tadi
Testlar: 1 o'tdi (jami 1)Test yashil, ilova esa production'da 404 oladi. Soxta fetch manzilga qaramadi — "nima so'rasang ham, mana menyu". Bu — yolg'on yashil ning eng keng tarqalgan turi. Testga expect(fakeFetch).toHaveBeenCalledWith("/api/menu") qo'shish mumkin edi — lekin buni har testda, har so'rov uchun, metod va sarlavhalari bilan yozish kerak. Hech kim yozmaydi.
Bizga haqiqiy serverga o'xshash narsa kerak: manzil va metodni biladigan, bilmagan so'roviga "bunday yo'l yo'q" deydigan.
2. G'oya: so'rovni manzil bo'yicha moslash
2.1 Kichik o'quv modeli
MSW'ning o'zi natija oynasida ishlamaydi (sababi — «Natija oynasida nega yo'q» bo'limida). G'oyani 15 qatorlik o'quv modeli bilan ko'ramiz. Diqqat: bu MSW emas — createFakeServer ni biz o'zimiz yozdik, u faqat MSW'ning ishlash tamoyilini ko'rsatadi. Haqiqiy MSW kodi «Handler yozish» bo'limidan boshlanadi. Chiqish uzun bo'ladi — uch joyga qarang: qaysi test × bo'ldi, <p role="status"> ichida qanday matn qoldi va oxirgi qatordagi jurnal.
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { afterEach, expect, test, vi } from "vitest";
// O'quv modeli: "handler"lar ro'yxati — metod + yo'l + javob
function createFakeServer(handlers) {
const log = [];
async function fakeFetch(input, init = {}) {
const method = init.method ?? "GET";
const path = new URL(input, "http://localhost").pathname;
log.push(`${method} ${path}`);
const h = handlers.find(
(x) => x.method === method && x.path === path,
);
if (!h) throw new TypeError(`Handler yo'q: ${method} ${path}`);
return h.reply(init);
}
return { fakeFetch, log };
}
function Menu({ url }) {
const [state, setState] = useState("Yuklanmoqda…");
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then((d) => setState(d.map((x) => x.name).join(", ")))
.catch((e) => setState(`Xato: ${e.message}`));
}, [url]);
return <p role="status">{state}</p>;
}
const server = createFakeServer([
{
method: "GET",
path: "/api/menu",
reply: () => Response.json([{ name: "Osh" }, { name: "Manti" }]),
},
]);
afterEach(() => vi.unstubAllGlobals());
test("to'g'ri manzil", async () => {
vi.stubGlobal("fetch", server.fakeFetch);
render(<Menu url="/api/menu" />);
expect(await screen.findByText("Osh, Manti")).toBeInTheDocument();
});
test("xato manzil — test yiqiladi", async () => {
vi.stubGlobal("fetch", server.fakeFetch);
render(<Menu url="/api/menyu" />);
expect(await screen.findByText("Osh, Manti")).toBeInTheDocument();
});
test("so'rovlar jurnali", () => {
console.log(server.log);
});Konsolda:
✓ to'g'ri manzil
× xato manzil — test yiqiladi
→ Unable to find an element with the text: Osh, Manti. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div
id="root"
/>
<div>
<p
role="status"
>
Xato: Handler yo'q: GET /api/menyu
</p>
</div>
</body>...
Ignored nodes: comments, script, style
<body>
<div
id="root"
/>
<div>
<p
role="status"
>
Xato: Handler yo'q: GET /api/menyu
</p>
</div>
</body>...
(2) ['GET /api/menu', 'GET /api/menyu']
✓ so'rovlar jurnali
Testlar: 1 yiqildi, 2 o'tdi (jami 3)Nima bo'ldi? Endi xato manzil darhol ko'rinadi: ekranda "Handler yo'q: GET /api/menyu". findBy xabarida DOM ikki marta chiqadi. Haqiqiy Vitest 5.0.4 + jsdom'da ham shunday — biz tekshirdik. <div id="root"> esa natija oynasining o'z ildizi, terminalda u yo'q. Jurnal oxirgi testda chiqdi: testlar e'lon tartibida ketma-ket ishlaydi, server esa ikkala so'rovni ham yozib qo'ydi.
Bu modelda uchta g'oya bor — MSW'ning asosi ham shular:
- Handler — "qaysi so'rovga (metod + yo'l) qanday javob".
- Moslanmagan so'rov — xato, jim javob emas.
- Ilova kodi o'zgarmaydi — u oddiy
fetchqiladi.
2.2 MSW qayerda ushlaydi
flowchart LR
A["Ilova: fetch('/api/menu')"] --> B{"Qayerda ishlayapti?"}
B -->|"Vitest / Node"| C["MSW interceptor<br/>(fetch, XMLHttpRequest)"]
B -->|"Brauzer"| D["Service Worker<br/>mockServiceWorker.js"]
C --> E["handlerlar"]
D --> E
E -->|"mos keldi"| F["soxta javob"]
E -->|"mos kelmadi"| G["onUnhandledFrame:<br/>error / warn / bypass"]Rasmda: Node'da (Vitest) MSW fetch va XMLHttpRequest ni o'zi ushlaydi (@mswjs/interceptors paketi). Brauzerda — Service Worker orqali: mockServiceWorker.js sahifaning hamma so'rovlarini ko'radi. Ikkala holatda ham bir xil handlerlar ishlaydi. Shuning uchun nomi "Mock Service Worker" — brauzerdagi yo'l birinchi bo'lgan.
3. Handler yozish
3.1 http va HttpResponse
// tekshiruv/msw/handlerlar.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/menu", () =>
HttpResponse.json([
{ id: 1, name: "Osh", price: 35000 },
{ id: 2, name: "Manti", price: 30000 },
]),
),
];| Qism | Ma'nosi |
|---|---|
http.get / post / put / patch / delete |
metod; http.all — istalgan metod |
"/api/menu" |
yo'l: nisbiy (sahifa manziliga nisbatan), to'liq URL yoki * bilan ("*/api/menu" — istalgan domen) |
"/api/taom/:id" |
parametr — params.id |
resolver ({ request, params, cookies }) |
so'rovni ko'rib, javob qaytaradi |
HttpResponse.json(data, { status }) |
JSON javob, Content-Type o'zi qo'yiladi |
HttpResponse.text("…") |
matn |
new HttpResponse(null, { status: 204 }) |
tanasiz javob |
HttpResponse.error() |
tarmoq xatosi — javob umuman yo'q |
request — standart Request obyekti (Fetch API): await request.json(), request.headers.get("accept"), new URL(request.url).searchParams.
3.2 Tana va parametrlar
http.post("*/api/orders", async ({ request }) => {
const body = await request.json(); // { dish: "Osh" }
return HttpResponse.json({ id: 7, ...body }, { status: 201 });
}),
http.patch("*/api/orders/:id", async ({ request, params }) => {
const changes = await request.json();
return HttpResponse.json({ id: Number(params.id), ...changes });
}),Handler — oddiy funksiya: o'zgaruvchida ro'yxat saqlab, POST da qo'shib, GET da qaytarsa — holatli soxta server bo'ladi. Kanon aynan shunday qildi (pastda).
4. setupServer: Vitest'da ulash
4.1 Hayot sikli
// tekshiruv/msw/node.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlerlar.ts";
export const server = setupServer(...handlers);// vitest-sozlash.ts
import { afterAll, afterEach } from "vitest";
import { server } from "./msw/node.ts";
server.listen({ onUnhandledFrame: "error" }); // modul darajasida!
afterEach(() => server.resetHandlers());
afterAll(() => server.close());| Chaqiruv | Nima qiladi |
|---|---|
server.listen(opts) |
ushlashni yoqadi |
server.use(...h) |
ustiga vaqtinchalik handler qo'shadi (birinchi tekshiriladi) |
server.resetHandlers() |
use qo'shganlarini olib tashlaydi — asl ro'yxat |
server.close() |
ushlashni o'chiradi |
Nega listen modul darajasida, beforeAll da emas — «Tuzoq: listen() kech qoldi» bo'limida.
4.2 onUnhandledFrame: handler yo'q so'rov
| Qiymat | Xulq |
|---|---|
"warn" (sukut) |
ogohlantirish, so'rov haqiqiy tarmoqqa ketadi |
"error" |
xato, so'rov yiqiladi |
"bypass" |
jim, tarmoqqa ketadi |
funksiya ({ frame, defaults }) |
o'zingiz hal qilasiz |
Testda — doim "error": test tarmoqqa chiqmasin va xato manzil darhol ko'rinsin. «Nega bu kerak?» dagi xato manzil, haqiqiy MSW bilan (Vitest 5.0.4, MSW 3.0.3 — biz ishga tushirdik):
stderr | sinov/msw/msw.test.jsx > MSW — menyu
[MSW] Error: intercepted a request without a matching request handler:
• GET /api/menyu
If you still wish to intercept this unhandled request, please create a request handler for it.
Read more: https://mswjs.io/docs/http/intercepting-requests
❯ sinov/msw/msw.test.jsx (1 test | 1 failed) 1048ms
× MSW — menyu 1041msTarjima: "handler'i yo'q so'rov ushlandi: GET /api/menyu. Uni ushlamoqchi bo'lsangiz, handler yarating." Qo'lda soxta bilan o'tgan test endi qizil — va sababi birinchi qatorda.
Eski maqolalar tuzog'i. MSW 3.0.0 (2026-09-28) gacha bu sozlama onUnhandledRequest deb atalardi. MSW 3 da u yo'q — va xato ham bermaydi, jim e'tiborsiz qoladi. Biz sinadik: server.listen({ onUnhandledRequest: "error" }) bilan handler'siz so'rov:
stderr | sinov/msw/eski.test.js > eski nom — onUnhandledRequest
[MSW] Warning: intercepted a request without a matching request handler:
• GET /api/boshqaError emas, Warning — sukutdagi "warn" ishladi va so'rov haqiqiy tarmoqqa ketdi. TypeScript bu xatoni ushlaydi (tsc 7.0.2, biz sinadik):
tssinov/a.ts(3,17): error TS2353: Object literal may only specify known properties, and 'onUnhandledRequest' does not exist in type 'PartialObjectDeep<SharedOptions, { recurseIntoArrays: false; allowUndefinedInNonTupleArrays: false; }>'.Tarjima: "obyekt literalida faqat ma'lum xususiyatlar bo'lishi mumkin, onUnhandledRequest bu turda yo'q". JavaScript'da esa — faqat Warning so'zi.
5. Komponent testi MSW bilan
5.1 Asosiy oqim va xato holatlari
Komponent — dars boshidagi Menu, manzili tuzatilgan va xato holati qo'shilgan:
import { useEffect, useState } from "react";
export function Menu() {
const [state, setState] = useState({ status: "loading" });
useEffect(() => {
fetch("/api/menu")
.then((r) => {
if (!r.ok) throw new Error(`Server ${r.status}`);
return r.json();
})
.then((dishes) => setState({ status: "done", dishes }))
.catch((e) =>
setState({ status: "error", message: e.message }),
);
}, []);
if (state.status === "loading") {
return <p role="status">Yuklanmoqda…</p>;
}
if (state.status === "error") {
return <p role="alert">Menyu yuklanmadi: {state.message}</p>;
}
return (
<ul aria-label="Menyu">
{state.dishes.map((d) => (
<li key={d.id}>
{d.name} — {d.price} so'm
</li>
))}
</ul>
);
}Test fayli:
import { render, screen } from "@testing-library/react";
import { delay, http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, expect, test } from "vitest";
import { Menu } from "../../src/Menu.jsx";
import { handlers } from "./handlerlar.js";
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledFrame: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test("muvaffaqiyat", async () => {
render(<Menu />);
const list = await screen.findByRole("list", { name: "Menyu" });
console.log(list.textContent);
});
test("server 500", async () => {
server.use(
http.get("/api/menu", () =>
HttpResponse.json({ xato: "Ichki xato" }, { status: 500 }),
),
);
render(<Menu />);
const alert = await screen.findByRole("alert");
console.log(alert.textContent);
});
test("tarmoq yo'q", async () => {
server.use(http.get("/api/menu", () => HttpResponse.error()));
render(<Menu />);
const alert = await screen.findByRole("alert");
console.log(alert.textContent);
});
test("sekin javob — yuklanish holati", async () => {
server.use(
http.get("/api/menu", async () => {
await delay(300);
return HttpResponse.json([
{ id: 1, name: "Osh", price: 35000 },
]);
}),
);
render(<Menu />);
const status = screen.getByRole("status");
expect(status).toHaveTextContent("Yuklanmoqda…");
const osh = await screen.findByText("Osh — 35000 so'm");
expect(osh).toBeInTheDocument();
});
test("keyingi test — asl handler qaytdi", async () => {
render(<Menu />);
expect(await screen.findAllByRole("listitem")).toHaveLength(2);
});Haqiqiy Vitest 5.0.4 (biz ishga tushirdik). Har stdout | bloki — bitta testning console.log i; 500 va "tarmoq yo'q" qatorlaridagi farqqa qarang:
stdout | sinov/msw/holatlar.test.jsx > muvaffaqiyat
Osh — 35000 so'mManti — 30000 so'm
stdout | sinov/msw/holatlar.test.jsx > server 500
Menyu yuklanmadi: Server 500
stdout | sinov/msw/holatlar.test.jsx > tarmoq yo'q
Menyu yuklanmadi: fetch failed
✓ sinov/msw/holatlar.test.jsx (5 tests) 563ms
✓ sekin javob — yuklanish holati 328msBu fayl bitta komponentning to'rt holatini sinadi — va komponent kodida bitta ham "test uchun" qator yo'q:
- 500 —
server.useyangi handler'ni ro'yxat boshiga qo'ydi, u birinchi mos keldi.afterEachdagiresetHandlersuni olib tashladi — oxirgi test yana ikki taomni ko'rdi. HttpResponse.error()— javob umuman yo'q:fetchTypeErrorbilan yiqiladi. Node'da xabarfetch failed, Chrome'da —Failed to fetch. Matnni foydalanuvchiga to'g'ridan chiqarmang — UI xabari o'zingizniki bo'lsin.delay(300)— haqiqiy tarmoqdek kechikish. Yuklanish holati endi ko'rinadi.delay()argumentsiz — tasodifiy "realistik" kechikish,delay("infinite")— hech qachon javob bermaydi (yuklanish holatini qotirish uchun).
Bir martalik handler ham bor: http.get(path, resolver, { once: true }) — birinchi so'rovdan keyin o'zini o'chiradi ("birinchi urinish yiqiladi, ikkinchisi o'tadi" ssenariysi uchun).
5.2 So'rovni tekshirish kerakmi?
MSW hujjati maslahat beradi: so'rovni emas, natijani tekshiring — "to'g'ri so'rov ketdi" emas, "foydalanuvchi to'g'ri natijani ko'rdi". Handler noto'g'ri so'rovga mos kelmaydi, demak natija baribir chiqmaydi.
Istisno — natijada ko'rinmaydigan narsa: tana maydonlari, sarlavhalar, "o'zgarish yo'q bo'lsa so'rov ketmasin". Kanon buni sorovlar() / tanalar() jurnali bilan qiladi — o'quv modelidagi log kabi.
Tekshirib ko'ring:
server.use(…)bilan 500 handler qo'shdingiz, lekinafterEach(() => server.resetHandlers())yo'q. Keyingi test fayllaridagi testlarga ta'sir qiladimi? Shu fayldagi keyingi testlarga-chi?
Javob
Shu fayldagi keyingi testlar hammasi 500 oladi — use qo'shgani close gacha turadi. Boshqa fayllarga esa ta'sir qilmaydi: Vitest har test faylini alohida muhitda (izolyatsiya) yurgizadi, server obyekti har faylda yangidan yaratiladi. Shuning uchun resetHandlers — sozlash faylida, hamma uchun.
6. Brauzerda: setupWorker
6.1 mockServiceWorker.js
Brauzerda MSW Service Worker orqali ishlaydi. Worker fayli loyihaning statik papkasiga ko'chiriladi:
npx msw init public/ --saveCopying the worker script at "C:\…\bahor\public"...
Worker script successfully copied!
- public\
Continue by describing the network in your application:
https://mswjs.io/docs/quick-start
Updating "msw.workerDirectory" at "C:\…\bahor\package.json"...--save — package.json ga "msw": { "workerDirectory": ["public"] } yozadi: msw paketi yangilansa, worker fayli ham o'zi yangilanadi. Fayl boshida versiya turadi (const PACKAGE_VERSION = '3.0.3') — paket va worker versiyasi mos bo'lishi kerak.
// src/mocks/brauzer.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlerlar.ts";
export const worker = setupWorker(...handlers);
// ishga tushirish: await worker.start({ onUnhandledFrame: "bypass" })6.2 Qayerda ishlatiladi
| Joy | Nima beradi | Kanonda |
|---|---|---|
| Vitest (Node, jsdom) | komponent va integratsiya testlari | bor — shu dars |
| Vitest Browser Mode | haqiqiy Chrome'da testlar, o'sha handlerlar | bor — Browser Mode |
| Dev server | backend tayyor bo'lmasa ham frontend ishlaydi | yo'q (ilova haqiqiy mashq API'ga ulanadi) |
| Storybook | story'lar soxta API bilan (msw-storybook-addon) |
yo'q — hujjat bo'yicha |
| Playwright e2e | — | o'z page.route i (Playwright II) |
Dev rejimida ulash import.meta.env.DEV bilan o'raladi — production bundle'ga MSW tushmasin. MSW 3 da yangi yo'l ham bor: msw/vite plagini (virtual:msw moduli) worker faylini o'zi beradi — hujjat bo'yicha, kanonda sinalmagan. msw-storybook-addon 3.0.3 (peer msw >=2) ham kanonda ishlatilmagan.
Ehtiyot: ilovaning o'z Service Worker'i bo'lsa (vazifalar da sw.js — PWA qismidan), ikkalasi bitta scope da to'qnashadi. Kanon Browser Mode da worker faylini ilova public/ iga emas, alohida test papkasiga qo'ygan.
7. Natija oynasida nega yo'q
Kanon msw/browser ni natija oynasida sinab ko'rdi — ishlamadi: import paytidayoq localStorage ga murojaat qiladi va sandbox'li iframe'da SecurityError oladi; Service Worker esa "shaffof bo'lmagan" (opaque) manbada ro'yxatdan o'tmaydi. Shuning uchun bu darsdagi MSW kodi — matn, chiqishi — haqiqiy terminaldan. Natija oynasida g'oyani «Kichik o'quv modeli» ko'rsatadi; boshqa jonli misollarda bog'liqlikni tashqaridan berish usulidan foydalanamiz (Vitest II).
8. Tuzoq: listen() kech qoldi
Kanon topgan va biz takrorlagan tuzoq. API klienti modul yuklanganda fetch ni o'ziga oladi:
// src/api.js — klient modul yuklanganda fetch'ni "eslab qoladi"
const send = globalThis.fetch;
export async function addOrder(dish) {
const res = await send("http://localhost:3000/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dish }),
});
if (!res.ok) throw new Error(`Server ${res.status}`);
return res.json();
}Sozlash faylida beforeAll(() => server.listen(…)) bilan test:
FAIL sinov2/post.test.js > buyurtma — so'rov tanasi
TypeError: fetch failed
…
stack: 'Error: connect ECONNREFUSED 127.0.0.1:3000\n' +So'rov MSW'ni chetlab, haqiqiy localhost:3000 ga ketdi — u yerda server yo'q (ECONNREFUSED — "ulanish rad etildi"). Sabab — tartib:
sequenceDiagram
participant S as vitest-sozlash.ts
participant T as test fayli
participant A as api.js
participant M as MSW
S->>S: beforeAll(listen) — faqat ro'yxatga
T->>A: import { addOrder }
A->>A: send = globalThis.fetch (asl!)
S->>M: beforeAll ishladi — fetch almashtirildi
T->>A: addOrder("Osh")
A->>A: send(...) — asl fetch, MSW'sizbeforeAll testlardan oldin ishlaydi, lekin test faylining importlaridan keyin. api.js o'shanda asl fetch ni eslab qolgan. Davosi — server.listen() ni sozlash faylining modul darajasida chaqirish: sozlash fayli test faylidan oldin yuklanadi. Shu o'zgarish bilan:
stdout | sinov2/post.test.js > buyurtma — so'rov tanasi
[ 'POST {"dish":"Osh"}' ]
✓ sinov2/post.test.js (1 test) 59msHujjatdagi namuna beforeAll(() => server.listen()) — u ko'p loyihada ishlaydi, chunki klient fetch ni har chaqiruvda global'dan oladi. "Eslab qoladigan" klient (API qatlami dagi kabi createApi({ fetch })) bo'lsa — modul darajasi.
9. Vazifalar qadami: MSW (holat 07)
9.1 O'rnatish va narx
git switch -c test/msw
npm i -D -E msw@3.0.3Narx: node_modules ga +7.9 MB, 25 paket (@mswjs/interceptors, type-fest, CLI uchun @inquirer/*). Ilova kodi o'zgarmadi, bundle — 206.7 KB gzip (MSW faqat testda). .env.test qo'shildi — Vitest test rejimida uni o'qiydi:
# Vitest (mode "test") — import.meta.env. 22/#07: API manzili MSW
# handlerlari bilan bir xil (tekshiruv/msw/soxta-api.ts); jsdom origin
VITE_API_URL=http://localhost:3000/api/mashq9.2 tekshiruv/msw/soxta-api.ts
Holatli soxta server: xotirada ro'yxat, POST qo'shadi, PATCH o'zgartiradi, keyingi GET o'zgarganini qaytaradi. Asosiy qismlar:
// .env.test dagi VITE_API_URL bilan bir xil yo'l (kalit — istalgan)
const YOL = "*/api/mashq/vazifalar/:kalit";
/** shu API ning istalgan so'rovi (ro'yxat ham, /:id ham) */
const HAMMASI = /\/api\/mashq\/vazifalar\//; http.post(YOL, async ({ request }) => {
const { matn } = (await yoz("POST", request)) as { matn: string };
const id = Math.max(0, ...holat.vazifalar.map((v) => v.id)) + 1;
const yangi = { id, matn, bajarildi: false };
holat.vazifalar.push(yangi);
holat.versiya++;
return HttpResponse.json(yangi, { status: 201 });
}),yoz() — so'rovni jurnalga yozadi ("POST", "PATCH /1") va tanasini (tanalar()). Bitta test uchun uch tayyor xulq:
/** Tarmoq xatosi: fetch TypeError ("Failed to fetch") bilan
* yiqiladi */
export const tarmoqYoq = http.all(HAMMASI, () => HttpResponse.error());
/** Har qanday so'rovga 500 */
export const server500 = http.all(HAMMASI, () =>
HttpResponse.json({ xato: "Ataylab" }, { status: 500 }),
);Uchinchisi — yozuvXatosi: GET o'tadi (return undefined — "keyingi handlerga"), yozuv so'rovlari 500 oladi. Resolver undefined qaytarsa, MSW keyingi mos handler'ni sinaydi — shu bilan bitta handler faqat bir qism so'rovlarni o'zgartiradi. Izohdagi "Failed to fetch" — Chrome matni; Node'da (Vitest) xabar fetch failed, klient esa ikkalasini bitta ApiXatosi ga aylantiradi.
9.3 vitest-sozlash.ts
// Tarmoq — MSW (22/#07): fetch HAQIQIY so'rov yuboradi, MSW uni
// tarmoq darajasida ushlaydi (tekshiruv/msw/soxta-api.ts). 17/#28
// dagi qo'lda vi.fn() fetch o'rniga: URL, metod va tana ham
// tekshiriladi. listen() — MODUL darajasida, beforeAll da EMAS:
// server-sinxron.ts api klientini import paytida yaratib
// globalThis.fetch ni o'shanda oladi; beforeAll test faylining
// importlaridan KEYIN ishlaydi — MSW kech qoladi (o'lchandi).
// Handler yo'q so'rov — xato (tarmoqqa chiqmaydi)
server.listen({ onUnhandledFrame: "error" });
afterEach(() => {
server.resetHandlers();
serverRoyxati();
});
afterAll(() => server.close());Bu — «listen() kech qoldi» tuzog'ining kanondagi asl nusxasi: useEffect III dagi vi.stubGlobal kech qolgan voqeaning davomi. serverRoyxati() — har testdan keyin server xotirasi bo'sh ro'yxatga qaytadi (RTL III dagi "global holat joyiga qaytsin" qoidasi).
9.4 Ko'chirish: 155 qator ketdi, 91 keldi
Olti fayl (App, harakatlar, VazifaSahifasi, VazifaTahriri, providerli-hooklar, qoralama) qo'lda soxtadan MSW'ga o'tdi. VazifaTahriri.test.tsx dagi farq:
function server({ xato = false } = {}) {
- const yozuvlar: string[] = [];
- vi.mocked(fetch).mockImplementation((_url, sozlama) => {
- const metod = sozlama?.method ?? "GET";
- if (metod === "GET") {
- return Promise.resolve(
- new Response(JSON.stringify({ versiya: 1, vazifalar: ROYXAT })),
- );
- }
- yozuvlar.push(`${metod} ${String(sozlama?.body)}`);
- …
- });
- return yozuvlar;
+ serverRoyxati(ROYXAT);
+ if (xato) msw.use(yozuvXatosi);
}Sof mantiq testlaridagi fetch tikuvi (apiKlient({ fetch }), oflayn navbat, sw) — qoldi: u yerda bog'liqlik parametr bilan beriladi, MSW kerak emas. Ikki usul bir loyihada yashaydi.
9.5 msw.test.tsx (5 test) — qo'lda soxta ko'rmagan narsalar
test("so'rovning o'zi: to'liq URL, metod, sarlavhalar", async () => {
let ushlangan: Request | undefined;
server.use(
http.patch("*/api/mashq/vazifalar/:kalit/:id", ({ request }) => {
ushlangan = request;
return HttpResponse.json({ id: 4, matn: "A", bajarildi: true });
}),
);
await api.yangila(4, { bajarildi: true });
expect(ushlangan?.method).toBe("PATCH");
expect(ushlangan?.url).toMatch(
/^http:\/\/localhost:3000\/api\/mashq\/vazifalar\/[^/]+\/4$/,
);
expect(ushlangan?.headers.get("content-type")).toBe(
"application/json",
);
expect(ushlangan?.headers.get("accept")).toBe("application/json");
});Qolgan to'rttasi: holatli server (POST → GET yangi vazifani qaytaradi, versiya 2), javob shartnomasi buzilsa (Zod) klient rad etadi, tarmoq yo'q → ApiXatosi { kod: "TARMOQ", holat: 0 }, handler yo'q so'rov yiqiladi (onUnhandledFrame: "error" ning o'zi sinaladi).
9.6 Biz topgan xato: bo'sh alert
MSW'ga o'tishda bitta test yiqildi — VazifaTahriri dagi "server rad etdi". Ilova to'g'ri edi, test noto'g'ri edi:
// 22/#07: <p role="alert"> DOIM bor (bo'sh) — findByRole
// darhol topadi. Qo'lda fetch soxtasi bir mikrotaskda javob
// berardi va test tasodifan o'tardi; MSW haqiqiy tarmoq kabi
// kechroq javob beradi → yiqildi. To'g'risi — MATN paydo
// bo'lishini kutish
await waitFor(() =>
expect(screen.getByRole("alert")).toHaveTextContent(
"Saqlanmadi: Ataylab",
),
);Bu — RTL II dagi «Element bor, matn hali yo'q» tuzog'ining o'zi. Qo'lda soxta shu qadar tez javob berardiki, xato matni findByRole tugaguncha ulgurardi. MSW real tarmoqdek bir necha tick kechikdi — va test o'z kamchiligini ko'rsatdi. Soxta server qanchalik haqiqiyga yaqin bo'lsa, test shunchalik ko'p narsani ushlaydi.
9.7 Natija va commit
Testlar 348/348 (343 + 5). npm test 10.08 s (holat 06 da 11.67 s — MSW sekinlashtirmadi). Qamrov — chegaradan o'tdi, satr 62.72 % (deyarli o'zgarmadi: testlar soni emas, sinalgan kod qatorlari sanaladi). Lint, tip, knip — toza; JS 206.7 KB.
git add -A
git commitCommit (kanon): test: MSW — qo'lda fetch soxtalari o'rniga tarmoq darajasidagi soxta server.
10. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
[MSW] Error: intercepted a request without a matching request handler |
manzil/metod mos emas | xabardagi so'rovni handler bilan solishtiring |
[MSW] Warning: … va ECONNREFUSED |
onUnhandledFrame berilmagan yoki eski nom onUnhandledRequest |
onUnhandledFrame: "error" |
| so'rov MSW'ni chetlab o'tdi | listen() kech (beforeAll), klient fetch ni oldin olgan |
listen() sozlash faylining modul darajasida |
| 500 keyingi testlarda ham | resetHandlers yo'q |
afterEach(() => server.resetHandlers()) |
| testlar bir-birining ma'lumotini ko'radi | holatli server tiklanmagan | afterEach da ro'yxatni tiklash |
| nisbiy yo'l mos kelmaydi | jsdom manzili boshqa yoki klient to'liq URL yuboradi | "*/api/…" |
Request is not defined (Jest) |
eski jsdom muhiti | Jest darsida |
11. Mashqlar
Mashqlar kurs/mashqlar/22/07-msw/ da. 2-mashq natija oynasida (o'quv modeli), 3-mashq — o'z loyihangizda MSW bilan.
1-mashq (oson): bo'sh joylarni to'ldiring
- GET handler:
http.("/api/menu", () => HttpResponse.json([])) - Tarmoq xatosi:
HttpResponse.() - Bitta test uchun xulq:
server.(handler) - Asl handlerlarga qaytish:
server.() - Handler yo'q so'rov — xato:
server.listen({ : "error" })
Yechim
- get — metod nomi;
http.all— istalgan metod. - error — javob yo'q,
fetchTypeErrorbilan yiqiladi. - use — ro'yxat boshiga, birinchi tekshiriladi.
- resetHandlers —
afterEachda. - onUnhandledFrame — MSW 3 nomi; 2.x da
onUnhandledRequest.
2-mashq (o'rta): o'quv modelida parametr va xato
«Kichik o'quv modeli» dagi createFakeServer ni kengaytiring: (1) yo'lda :id parametri (/api/taom/:id — reply({ params }) ga params.id keladi); (2) use(handler) — ro'yxat boshiga qo'shadi, reset() — olib tashlaydi. Test: DishName komponenti /api/taom/2 ni so'raydi va nomni chiqaradi; ikkinchi testda use bilan 404 qaytarib, «Taom topilmadi» chiqishini, uchinchi testda (reset dan keyin) yana nom chiqishini tekshiring.
Ishora: yo'lni / bo'yicha bo'lib, : bilan boshlangan qismni parametr deb oling.
Yechim
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
function match(pattern, path) {
const a = pattern.split("/");
const b = path.split("/");
if (a.length !== b.length) return null;
const params = {};
for (let i = 0; i < a.length; i++) {
if (a[i].startsWith(":")) params[a[i].slice(1)] = b[i];
else if (a[i] !== b[i]) return null;
}
return params;
}
function createFakeServer(base) {
let extra = [];
async function fakeFetch(input, init = {}) {
const method = init.method ?? "GET";
const path = new URL(input, "http://localhost").pathname;
for (const h of [...extra, ...base]) {
const params = h.method === method && match(h.path, path);
if (params) return h.reply({ params });
}
throw new TypeError(`Handler yo'q: ${method} ${path}`);
}
return {
fakeFetch,
use: (h) => { extra = [h, ...extra]; },
reset: () => { extra = []; },
};
}
const DISHES = { 1: "Osh", 2: "Manti" };
const server = createFakeServer([
{
method: "GET",
path: "/api/taom/:id",
reply: ({ params }) => Response.json({ name: DISHES[params.id] }),
},
]);
function DishName({ id }) {
const [text, setText] = useState("…");
useEffect(() => {
fetch(`/api/taom/${id}`).then(async (r) => {
setText(r.ok ? (await r.json()).name : "Taom topilmadi");
});
}, [id]);
return <h2>{text}</h2>;
}
beforeEach(() => vi.stubGlobal("fetch", server.fakeFetch));
afterEach(() => {
server.reset();
vi.unstubAllGlobals();
});
test("parametr — 2-taom", async () => {
render(<DishName id={2} />);
expect(await screen.findByText("Manti")).toBeInTheDocument();
});
test("use — 404", async () => {
server.use({
method: "GET",
path: "/api/taom/:id",
reply: () => Response.json({}, { status: 404 }),
});
render(<DishName id={2} />);
const h = await screen.findByText("Taom topilmadi");
console.log(h.tagName);
});
test("reset — asl handler", async () => {
render(<DishName id={1} />);
expect(await screen.findByText("Osh")).toBeInTheDocument();
});Konsolda:
✓ parametr — 2-taom
H2
✓ use — 404
✓ reset — asl handler
Testlar: 3 o'tdi (jami 3)use qo'shgani extra boshida — birinchi tekshiriladi; reset uni olib tashlaydi. MSW'da bu server.use va server.resetHandlers, :id esa handler resolver'idagi params.id.
3-mashq (qiyin): o'z loyihangizda MSW
DishName ni (2-mashqdan) haqiqiy MSW bilan sinang: http.get("/api/taom/:id", …) handler, setupServer, uchta test — topildi, server.use bilan 404, va HttpResponse.error() bilan tarmoq xatosi. Uchinchi holat uchun komponentga catch qo'shing: «Internet yo'q».
Yechim
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { useEffect, useState } from "react";
import { afterAll, afterEach, beforeAll, expect, test } from "vitest";
const DISHES = { 1: "Osh", 2: "Manti" };
const server = setupServer(
http.get("/api/taom/:id", ({ params }) =>
HttpResponse.json({ name: DISHES[params.id] }),
),
);
beforeAll(() => server.listen({ onUnhandledFrame: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
function DishName({ id }) {
const [text, setText] = useState("…");
useEffect(() => {
fetch(`/api/taom/${id}`)
.then(async (r) => {
setText(r.ok ? (await r.json()).name : "Taom topilmadi");
})
.catch(() => setText("Internet yo'q"));
}, [id]);
return <h2>{text}</h2>;
}
test("topildi", async () => {
render(<DishName id={2} />);
expect(await screen.findByText("Manti")).toBeInTheDocument();
});
test("404", async () => {
server.use(
http.get("/api/taom/:id", () =>
HttpResponse.json({}, { status: 404 }),
),
);
render(<DishName id={2} />);
const msg = await screen.findByText("Taom topilmadi");
expect(msg).toBeInTheDocument();
});
test("tarmoq xatosi", async () => {
server.use(http.get("/api/taom/:id", () => HttpResponse.error()));
render(<DishName id={1} />);
const msg = await screen.findByText("Internet yo'q");
expect(msg).toBeInTheDocument();
});Haqiqiy Vitest 5.0.4 + MSW 3.0.3 + jsdom (biz ishga tushirdik):
✓ sinov/taom.test.jsx (3 tests) 114ms
Test Files 1 passed (1)
Tests 3 passed (3)Nisbiy yo'l (/api/taom/2) jsdom'da sahifa manziliga (http://localhost:3000/) nisbatan to'liq URL bo'ladi — handler'dagi nisbiy yo'l ham xuddi shunday hisoblanadi, shuning uchun mos keladi.
4-mashq: Vazifalar qadami
test/msw branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: msw@3.0.3, .env.test, tekshiruv/msw/ (soxta-api.ts, node.ts), vitest-sozlash.ts, olti faylni ko'chirish va msw.test.tsx (5).
Yechim
Tekshiruv ro'yxati:
npm test— 348 test;msw.test.tsx— 5.vitest-sozlash.tsdaserver.listen(…)nibeforeAllga ko'chiring — qaysi testlar yiqiladi va xabarda qaysi manzil bor? Qaytaring.onUnhandledFramenionUnhandledRequestga almashtiring:npm run tipnima deydi? Testlar-chi?VazifaTahriridagiwaitForni eskifindByRole("alert")ga qaytaring — MSW bilan yiqilishini o'zingiz ko'ring.soxta-api.tsdagiYOLni/api/mashq/vazifa/ga (ko'plik qo'shimchasisiz) o'zgartiring — nechta test qizil bo'ladi? Qo'lda soxta bilan bu xatoni hech biri ko'rmas edi.
12. Real ishda
- Frontend va backend parallel: API shartnomasi (OpenAPI) kelishilgach, frontend MSW handlerlari bilan ishlay boshlaydi — backend tayyor bo'lishini kutmaydi. Handlerlarni OpenAPI'dan avtomat yasaydigan vositalar ham bor.
- Bitta handler — ko'p joyda: test, Browser Mode, dev, Storybook. Handlerlar
src/mocks/yokitekshiruv/msw/da, ilova kodidan alohida. - Chegara: MSW frontend'ni sinaydi, backend'ni emas. Haqiqiy server bilan shartnoma mosligi — Backend test strategiyasi va e2e'da.
- Intervyu savollari: "MSW
vi.fnfetch'dan nimasi bilan yaxshi?", "Node'da va brauzerda MSW qanday ushlaydi?", "server.usevaresetHandlers?", "handler yo'q so'rov bilan nima qilasiz?".
Xulosa
- Qo'lda
fetchsoxtasi manzil va metodga qaramaydi — xato URL bilan ham test o'tadi; MSW so'rovni handler'ga moslaydi, moslanmaganini xato qiladi. - Handler:
http.get/post/…(yo'l, resolver),HttpResponse.json/text/error,params,request.json(). - Vitest'da
setupServer(msw/node):listen({ onUnhandledFrame: "error" })modul darajasida,afterEach—resetHandlers,afterAll—close. MSW 3 daonUnhandledRequestyo'q — jim e'tiborsiz qoladi. - Bitta test uchun xulq —
server.use: 500,HttpResponse.error()(Node:fetch failed),delay(). - Brauzerda —
setupWorker+npx msw init public/ --save; natija oynasida MSW ishlamaydi (sandbox). vazifalar: holatli soxta server, 6 fayl ko'chdi (−155 / +91 qator), 348 test; MSW real kechikishi yashirin test xatosini ochdi.
Keyingi dars: Vitest Browser Mode: haqiqiy brauzerda komponent testi — jsdom'da ochilmagan dialoglarni haqiqiy Chrome'da sinaymiz, o'sha MSW handlerlari setupWorker bilan.
Manbalar
- MSW hujjati: Quick start,
http,HttpResponse,setupServer,setupWorker,delay, "Network behavior overrides" (server.use), "Avoid request assertions" (mswjs.io/docs) - MSW 3 paketi (3.0.3, 2026-10-10):
SharedOptions.onUnhandledFrame,msw/viteplagini — paket ichidagi tur fayllari va izohlar msw-storybook-addon3.0.3 — npm (peermsw >=2), kanonda ishlatilmagan- Haqiqiy chiqishlar: Vitest 5.0.4, MSW 3.0.3, RTL 16.3.3, jsdom 30.1.2, Node 24.21.0 (2026-10-20); kanon
vazifalarholat 07 (2026-10-20)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!