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

MSW: API'ni tarmoq darajasida mock qilish — handler, setupServer va xato holatlari

Qisqacha: Qo'lda yozilgan fetch soxtasi (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 haqiqiy fetch qiladi, 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()), keyin resetHandlers(). Handler yo'q so'rov — onUnhandledFrame: "error" bilan xato.

Bu darsda

  • Qo'lda fetch soxtasi nimani ko'rmasligini va MSW buni qanday ushlashini ko'rasiz.
  • http.get/post/patch/delete, HttpResponse, URL parametrlari bilan handler yozasiz.
  • setupServer, listen / resetHandlers / close va onUnhandledFrame ni sozlaysiz.
  • server.use bilan 500, tarmoq xatosi va sekin javobni sinaysiz.
  • setupWorker, mockServiceWorker.js va 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:

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

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

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

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

  1. Handler — "qaysi so'rovga (metod + yo'l) qanday javob".
  2. Moslanmagan so'rov — xato, jim javob emas.
  3. Ilova kodi o'zgarmaydi — u oddiy fetch qiladi.

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

ts
// 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

ts
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

ts
// tekshiruv/msw/node.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlerlar.ts";

export const server = setupServer(...handlers);
ts
// 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):

text
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 1041ms

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

text
stderr | sinov/msw/eski.test.js > eski nom — onUnhandledRequest
[MSW] Warning: intercepted a request without a matching request handler:

  • GET /api/boshqa

Error emas, Warning — sukutdagi "warn" ishladi va so'rov haqiqiy tarmoqqa ketdi. TypeScript bu xatoni ushlaydi (tsc 7.0.2, biz sinadik):

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

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

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

text
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 328ms

Bu fayl bitta komponentning to'rt holatini sinadi — va komponent kodida bitta ham "test uchun" qator yo'q:

  • 500 — server.use yangi handler'ni ro'yxat boshiga qo'ydi, u birinchi mos keldi. afterEach dagi resetHandlers uni olib tashladi — oxirgi test yana ikki taomni ko'rdi.
  • HttpResponse.error() — javob umuman yo'q: fetch TypeError bilan yiqiladi. Node'da xabar fetch 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, lekin afterEach(() => 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:

bash
npx msw init public/ --save
text
Copying 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.

ts
// 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:

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

text
 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'siz

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

text
stdout | sinov2/post.test.js > buyurtma — so'rov tanasi
[ 'POST {"dish":"Osh"}' ]

 ✓ sinov2/post.test.js (1 test) 59ms

Hujjatdagi 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

bash
git switch -c test/msw
npm i -D -E msw@3.0.3

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

text
# 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/mashq

9.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:

ts
// .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\//;
ts
  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:

ts
/** 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

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:

diff
 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

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

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

bash
git add -A
git commit

Commit (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, fetch TypeError bilan yiqiladi.
  • use — ro'yxat boshiga, birinchi tekshiriladi.
  • resetHandlers — afterEach da.
  • 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
jsx
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:

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

text
 ✓ 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.ts da server.listen(…) ni beforeAll ga ko'chiring — qaysi testlar yiqiladi va xabarda qaysi manzil bor? Qaytaring.
  • onUnhandledFrame ni onUnhandledRequest ga almashtiring: npm run tip nima deydi? Testlar-chi?
  • VazifaTahriri dagi waitFor ni eski findByRole("alert") ga qaytaring — MSW bilan yiqilishini o'zingiz ko'ring.
  • soxta-api.ts dagi YOL ni /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/ yoki tekshiruv/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.fn fetch'dan nimasi bilan yaxshi?", "Node'da va brauzerda MSW qanday ushlaydi?", "server.use va resetHandlers?", "handler yo'q so'rov bilan nima qilasiz?".

Xulosa

  • Qo'lda fetch soxtasi 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 da onUnhandledRequest yo'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/vite plagini — paket ichidagi tur fayllari va izohlar
  • msw-storybook-addon 3.0.3 — npm (peer msw >=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 vazifalar holat 07 (2026-10-20)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
MSW: API'ni tarmoq darajasida mock qilish — handler, setupServer va xato holatlari — IlmHamroh