MSW — API mock qilish, real so'rovlarsiz test

~20 daq · React · Testing

Muammodan boshlaylik

76-darsda API ni soxtalashtirishning ikki usulini ko'rdik — ikkalasining ham kamchiligi bor:

  • fetch ni mock qilish — Axios ga o'tsangiz, testlar buziladi
  • vi.mock('../api/users') — API modulining o'zi (URL, header lar, xatolarni qayta ishlash) tekshirilmaydi; har test faylida mock larni takrorlash

Va yana bir muammo: backend hali tayyor emas. Frontend jamoasi kutib o'tirishi kerakmi?

MSW (Mock Service Worker) so'rovlarni tarmoq darajasida ushlaydi. Sizning kodingiz — komponent, hook, fetch yoki Axios, API moduli — haqiqiy so'rov yuboradi; MSW uni server ga yetib bormasdan ushlab, soxta javob qaytaradi. Kod bu farqni sezmaydi.

Komponent → useQuery → api/users.js → fetch/axios → [MSW] ✋ → soxta javob
                                                       ↑
                                       haqiqiy server ga bormaydi

O'rnatish

npm install -D msw

Handler lar — soxta API

// src/mocks/handlers.js
import { http, HttpResponse } from 'msw';

const foydalanuvchilar = [
  { id: 1, ism: 'Ali Valiyev', email: 'ali@mail.uz' },
  { id: 2, ism: 'Zarina Karimova', email: 'zarina@mail.uz' },
];

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json(foydalanuvchilar);
  }),

  http.get('/api/users/:id', ({ params }) => {
    const user = foydalanuvchilar.find((u) => u.id === Number(params.id));
    if (!user) {
      return HttpResponse.json({ xabar: 'Topilmadi' }, { status: 404 });
    }
    return HttpResponse.json(user);
  }),

  http.post('/api/users', async ({ request }) => {
    const yangi = await request.json();
    return HttpResponse.json({ id: 3, ...yangi }, { status: 201 });
  }),
];
  • http.get, http.post, http.put, http.patch, http.delete — metod va URL
  • :id — URL parametri → params.id
  • request — haqiqiy Request obyekti: request.json(), request.headers, new URL(request.url).searchParams
  • HttpResponse.json(malumot, { status }) — JSON javob

Handler lar — Express route lariga o'xshaydi. Ular API ning kichik soxta versiyasi.

Test uchun server

// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// src/test/setup.js
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { server } from '../mocks/server';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => {
  server.resetHandlers();     // har testdan keyin — test ichidagi o'zgarishlarni bekor qilish
  cleanup();
});
afterAll(() => server.close());

onUnhandledRequest: 'error' — handler yozilmagan so'rov yuborilsa, test yiqiladi. Shunda hech qanday so'rov tasodifan haqiqiy serverga ketmaydi va unutilgan handler lar darhol ko'rinadi.


Testlarda

Endi testlarda hech qanday mock yo'q:

it('foydalanuvchilar ro\'yxatini ko\'rsatadi', async () => {
  renderWithProviders(<Foydalanuvchilar />);

  expect(await screen.findByText('Ali Valiyev')).toBeInTheDocument();
  expect(screen.getByText('Zarina Karimova')).toBeInTheDocument();
});

Komponent → TanStack Query → API moduli → fetch → MSW → javob. Butun zanjir haqiqiy, faqat server soxta.

Bitta test uchun boshqa javob — server.use

import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';

it('server xatosida xabar ko\'rsatadi', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ xabar: 'Ichki xato' }, { status: 500 });
    })
  );

  renderWithProviders(<Foydalanuvchilar />);

  expect(await screen.findByRole('alert')).toHaveTextContent(/xato/i);
});

it("bo'sh ro'yxatda xabar ko'rsatadi", async () => {
  server.use(http.get('/api/users', () => HttpResponse.json([])));

  renderWithProviders(<Foydalanuvchilar />);

  expect(await screen.findByText("Hali foydalanuvchilar yo'q")).toBeInTheDocument();
});

server.use — handler ni vaqtincha almashtiradi; afterEach dagi resetHandlers() uni asl holatiga qaytaradi.

Maxsus holatlar

import { http, HttpResponse, delay } from 'msw';

// Tarmoq uzilishi
http.get('/api/users', () => HttpResponse.error());

// Sekin javob — skeleton ni tekshirish uchun
http.get('/api/users', async () => {
  await delay(2000);
  return HttpResponse.json([]);
});

// Abadiy kutish
http.get('/api/users', async () => {
  await delay('infinite');
});

So'rov to'g'ri yuborilganini tekshirish

it('forma ma\'lumotlarini to\'g\'ri yuboradi', async () => {
  let yuborilgan;
  server.use(
    http.post('/api/users', async ({ request }) => {
      yuborilgan = await request.json();
      return HttpResponse.json({ id: 3, ...yuborilgan }, { status: 201 });
    })
  );

  const user = userEvent.setup();
  renderWithProviders(<YangiFoydalanuvchi />);

  await user.type(screen.getByLabelText('Ism'), 'Sardor');
  await user.type(screen.getByLabelText('Email'), 'sardor@mail.uz');
  await user.click(screen.getByRole('button', { name: 'Saqlash' }));

  expect(await screen.findByText("Foydalanuvchi qo'shildi")).toBeInTheDocument();
  expect(yuborilgan).toEqual({ ism: 'Sardor', email: 'sardor@mail.uz' });
});

Brauzerda — backend siz ishlab chiqish

MSW ning eng kuchli tomoni: o'sha handler lar brauzerda ham ishlaydi. Backend tayyor bo'lmasa ham frontend to'liq ishlaydi:

npx msw init public/ --save

Bu buyruq public/mockServiceWorker.js faylini yaratadi — Service Worker so'rovlarni ushlaydi.

// src/mocks/browser.js
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
// src/main.jsx
async function mocklarniYoq() {
  if (import.meta.env.DEV && import.meta.env.VITE_MOCK === 'true') {
    const { worker } = await import('./mocks/browser');
    await worker.start({ onUnhandledRequest: 'bypass' });
  }
}

mocklarniYoq().then(() => {
  createRoot(document.getElementById('root')).render(<App />);
});
VITE_MOCK=true npm run dev
  • Faqat development da va kerak bo'lganda yoqiladi; dynamic import (69-dars) — MSW production bundle ga kirmaydi
  • Network tabida so'rovlar ko'rinadi — xuddi haqiqiy kabi
  • onUnhandledRequest: 'bypass' — handler yo'q so'rovlar (rasmlar, shriftlar) haqiqiy tarmoqqa o'tadi

Bitta handler to'plami — testlar, development, Storybook va demo uchun.

[!TIP] Handler lardagi soxta ma'lumot haqiqiy API ga mos bo'lishi kerak — aks holda testlar o'tadi, production esa buziladi. Backend OpenAPI (Swagger) hujjati bersa, undan handler larni avtomatik yaratish vositalari bor. Kamida maydon nomlari va turlarini backend jamoasi bilan kelishib oling.


Nima uchun shunday?

Nima uchun so'rovlarni tarmoq darajasida ushlash fetch yoki modulni mock qilishdan yaxshiroq?

Chunki mock qanchalik pastda bo'lsa, test shunchalik ko'p haqiqiy kodni tekshiradi. Modulni mock qilsangiz — URL to'g'ri tuzilganmi, header lar qo'shilganmi, javob to'g'ri qayta ishlanganmi, 404 da xato tashlanadimi — hech biri tekshirilmaydi; aynan shu joylarda esa xatolar ko'p bo'ladi. MSW ilova va tashqi dunyo o'rtasidagi eng oxirgi chegarada turadi: undan oldingi hamma narsa — haqiqiy. Bundan tashqari, testlar implementatsiyaga bog'lanmaydi: fetch dan Axios ga, TanStack Query dan RTK Query ga o'tsangiz ham, testlar o'zgarmasdan o'tadi — chunki ular "qanday so'rov yuborildi" ni emas, "foydalanuvchi nima ko'rdi" ni tekshiradi.


Xulosa

Eslab qoling:

  • MSW so'rovlarni tarmoq darajasida ushlaydi; kod (fetch/Axios, API modul, hook) — haqiqiy
  • Handler lar: http.get('/api/users/:id', ({ params, request }) => HttpResponse.json(...))
  • Node (testlar): setupServer(...handlers); listen({ onUnhandledRequest: 'error' }), resetHandlers(), close()
  • Bitta test uchun — server.use(...): xato (500), bo'sh ro'yxat, HttpResponse.error(), delay
  • Yuborilgan ma'lumotni handler ichida request.json() bilan tekshirish
  • Brauzer: npx msw init public/, setupWorker, faqat DEV da dynamic import bilan
  • Bitta handler to'plami — testlar, development, demo; ma'lumot haqiqiy API ga mos bo'lsin

Keyingi bo'lim

12-bo'lim: Ilg'or mavzular — Error Boundary, Portals, Suspense, Concurrent rendering va Server Components.