React Testing Library — async testlar, mock

~20 daq · React · Testing

Muammodan boshlaylik

Foydalanuvchi profili komponenti:

function Profil({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then((j) => j.json())
      .then(setUser);
  }, [id]);

  if (!user) return <p>Yuklanmoqda...</p>;
  return <h1>{user.ism}</h1>;
}

75-darsdagi kabi test yozamiz:

it('foydalanuvchi ismini ko\'rsatadi', () => {
  render(<Profil id={1} />);
  expect(screen.getByRole('heading', { name: 'Ali' })).toBeInTheDocument();   // ❌
});

Test yiqiladi: Unable to find role "heading". Sabab — render dan keyin darhol tekshirdik, ekranda hali "Yuklanmoqda...". Ma'lumot keyinroq keladi. Bundan tashqari, test haqiqiy serverga so'rov yuborishga harakat qildi — u esa testda yo'q.

Ikki muammo: kutish va soxta API.


Kutish — findBy

it('foydalanuvchi ismini ko\'rsatadi', async () => {
  render(<Profil id={1} />);

  expect(screen.getByText('Yuklanmoqda...')).toBeInTheDocument();

  expect(await screen.findByRole('heading', { name: 'Ali' })).toBeInTheDocument();
});

findBy... — Promise qaytaradi va element paydo bo'lguncha kutadi (standart holatda 1000 ms gacha, har 50 ms da tekshirib). Topilsa — element; vaqt tugasa — xato.

await screen.findByText('Saqlandi', {}, { timeout: 3000 });   // uzoqroq kutish

waitFor — shart bajarilguncha

findBy — element paydo bo'lishini kutish uchun. Boshqa shartlar uchun — waitFor:

import { waitFor } from '@testing-library/react';

// Funksiya chaqirilishini kutish
await waitFor(() => {
  expect(onSaqla).toHaveBeenCalledTimes(1);
});

// Element YO'QOLISHINI kutish
await waitFor(() => {
  expect(screen.queryByText('Yuklanmoqda...')).not.toBeInTheDocument();
});

Yo'qolish uchun maxsus funksiya ham bor:

import { waitForElementToBeRemoved } from '@testing-library/react';

await waitForElementToBeRemoved(() => screen.queryByText('Yuklanmoqda...'));

[!WARNING] waitFor ichida faqat tekshiruv bo'lsin, harakat emas. waitFor(() => user.click(...)) — tugma o'nlab marta bosilishi mumkin, chunki waitFor funksiyani shart bajarilguncha takror-takror chaqiradi. Shuningdek waitFor ichiga bir nechta expect qo'ymang — birinchisi o'tib ikkinchisi yiqilsa, xato sababini topish qiyin bo'ladi.

act ogohlantirishi

Ba'zan konsolda:

Warning: An update to Profil inside a test was not wrapped in act(...)

Bu — test tugagandan keyin komponentda state yangilangan degani: siz async natijani kutmagansiz. Yechim deyarli har doim — findBy yoki waitFor bilan oxirgi o'zgarishni kutish. render, userEvent, findBy va waitFor o'zlari act ga o'ralgan — act ni qo'lda yozish kamdan-kam kerak.


API ni soxtalashtirish

1-usul: fetch ni mock qilish

beforeEach(() => {
  vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({ id: 1, ism: 'Ali' }),
  });
});

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

it('foydalanuvchi ismini ko\'rsatadi', async () => {
  render(<Profil id={1} />);
  expect(await screen.findByRole('heading', { name: 'Ali' })).toBeInTheDocument();
  expect(fetch).toHaveBeenCalledWith('/api/users/1');
});

Ishlaydi, lekin mo'rt: fetch ni Axios ga almashtirsangiz — test buziladi; json() qanday chaqirilishini bilish kerak; bir nechta turli so'rov bo'lsa — murakkablashadi.

2-usul: API modulini mock qilish

Agar so'rovlar alohida modulda bo'lsa (52-dars, api/ papkasi):

import * as usersApi from '../api/users';

vi.mock('../api/users');

it('foydalanuvchi ismini ko\'rsatadi', async () => {
  usersApi.getUser.mockResolvedValue({ id: 1, ism: 'Ali' });

  renderWithProviders(<Profil id={1} />);

  expect(await screen.findByRole('heading', { name: 'Ali' })).toBeInTheDocument();
  expect(usersApi.getUser).toHaveBeenCalledWith(1);
});

Toza va tez. Lekin api/users.js ning o'zi (URL, header lar, xatolarni qayta ishlash) tekshirilmaydi.

3-usul: MSW — tarmoq darajasida

Eng haqiqatga yaqin usul — so'rovlarni tarmoq darajasida ushlash: komponent, fetch/Axios, API moduli — hammasi haqiqiy, faqat server soxta. Bu — MSW, 78-dars.

Usul Haqiqatga yaqinlik Qulaylik Qachon
fetch mock Past O'rtacha Bitta oddiy so'rov
Modul mock (vi.mock) O'rtacha Yuqori Tez unit testlar
MSW Yuqori Yuqori (sozlangandan keyin) Integration testlar — tavsiya etiladi

Holatlarni tekshirish

Har bir async komponent uchun uchta holatni tekshiring (49-dars):

describe('Profil', () => {
  it('yuklanayotganda skeleton ko\'rsatadi', () => {
    usersApi.getUser.mockReturnValue(new Promise(() => {}));   // hech qachon tugamaydi
    renderWithProviders(<Profil id={1} />);
    expect(screen.getByText('Yuklanmoqda...')).toBeInTheDocument();
  });

  it('muvaffaqiyatda ismni ko\'rsatadi', async () => {
    usersApi.getUser.mockResolvedValue({ id: 1, ism: 'Ali' });
    renderWithProviders(<Profil id={1} />);
    expect(await screen.findByRole('heading', { name: 'Ali' })).toBeInTheDocument();
  });

  it('xatoda xabar va qayta urinish tugmasini ko\'rsatadi', async () => {
    usersApi.getUser.mockRejectedValue(new Error('Server ishlamayapti'));
    renderWithProviders(<Profil id={1} />);
    expect(await screen.findByRole('alert')).toHaveTextContent('Server ishlamayapti');
    expect(screen.getByRole('button', { name: 'Qayta urinish' })).toBeInTheDocument();
  });
});

Xato holati — eng ko'p unutiladigan va eng ko'p buziladigan qism.


Komponentlarni mock qilish

Ba'zan og'ir bola komponent (xarita, grafik, video pleer) testda kerak emas va jsdom da ishlamaydi:

vi.mock('../components/Xarita', () => ({
  default: ({ manzil }) => <div>Xarita: {manzil}</div>,
}));

Endi Xarita — oddiy matn. Lekin ehtiyot bo'ling: mock qilingan komponentning haqiqiy xatti-harakati tekshirilmaydi.


Router bilan

it('mahsulot bosilganda sahifasiga o\'tadi', async () => {
  const user = userEvent.setup();

  render(
    <MemoryRouter initialEntries={['/katalog']}>
      <Routes>
        <Route path="/katalog" element={<Katalog />} />
        <Route path="/mahsulot/:id" element={<p>Mahsulot sahifasi</p>} />
      </Routes>
    </MemoryRouter>
  );

  await user.click(await screen.findByRole('link', { name: 'Kitob' }));

  expect(screen.getByText('Mahsulot sahifasi')).toBeInTheDocument();
});

useNavigate ni mock qilish o'rniga — haqiqiy Router bilan oddiy "qo'nish" sahifasi. Natija — foydalanuvchi ko'radigan narsa.


Vaqtga bog'liq kod

Debounce li qidiruv (25-dars):

it('300 ms dan keyin qidiradi', async () => {
  vi.useFakeTimers({ shouldAdvanceTime: true });
  const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
  const onQidir = vi.fn();
  render(<Qidiruv onQidir={onQidir} />);

  await user.type(screen.getByRole('searchbox'), 'react');
  expect(onQidir).not.toHaveBeenCalled();

  await vi.advanceTimersByTimeAsync(300);
  expect(onQidir).toHaveBeenCalledWith('react');

  vi.useRealTimers();
});

Soxta taymerlar bilan userEvent.setup ga advanceTimers berish shart — aks holda user.type abadiy kutib qoladi.


Nima uchun shunday?

Nima uchun testlarda haqiqiy serverga so'rov yubormaslik kerak?

Chunki test deterministik bo'lishi kerak: bir xil kod — har safar bir xil natija. Haqiqiy server bu kafolatni buzadi: u ishlamay qolishi, sekin javob berishi, ma'lumoti o'zgarishi mumkin (kimdir "Ali" ni "Vali" ga o'zgartirdi — test yiqildi, kodingizda esa xato yo'q). Bunday "tasodifan yiqiladigan" (flaky) testlar eng zararli: jamoa ularga ishonmay qo'yadi va haqiqiy xatolar ham e'tiborsiz qoladi. Bundan tashqari, server bilan xato holatlarini (500, timeout, bo'sh ro'yxat) qayta hosil qilish deyarli mumkin emas — soxta API bilan esa bitta qator. Mock — testni tashqi dunyodan ajratish, u faqat sizning kodingizni tekshirsin.


Xulosa

Eslab qoling:

  • Async natija — await screen.findBy... (paydo bo'lishini kutadi)
  • Boshqa shartlar — await waitFor(() => expect(...)); ichida faqat tekshiruv
  • Yo'qolish — waitForElementToBeRemoved yoki waitFor + queryBy
  • "not wrapped in act" — oxirgi async o'zgarishni kutmagansiz
  • API: fetch mock (oddiy), vi.mock (modul), MSW (eng yaxshi — 78-dars)
  • Uch holat: yuklanish, muvaffaqiyat, xato — hammasini tekshiring
  • Router — MemoryRouter + haqiqiy Routes
  • Soxta taymerlar — userEvent.setup({ advanceTimers })

Keyingi dars

77-dars: React Testing Library — Custom Hook testlash