React Testing Library — async testlar, mock
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]
waitForichida faqat tekshiruv bo'lsin, harakat emas.waitFor(() => user.click(...))— tugma o'nlab marta bosilishi mumkin, chunkiwaitForfunksiyani shart bajarilguncha takror-takror chaqiradi. ShuningdekwaitForichiga bir nechtaexpectqo'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 —
waitForElementToBeRemovedyokiwaitFor+queryBy - "not wrapped in act" — oxirgi async o'zgarishni kutmagansiz
- API:
fetchmock (oddiy),vi.mock(modul), MSW (eng yaxshi — 78-dars) - Uch holat: yuklanish, muvaffaqiyat, xato — hammasini tekshiring
- Router —
MemoryRouter+ haqiqiyRoutes - Soxta taymerlar —
userEvent.setup({ advanceTimers })
Keyingi dars
77-dars: React Testing Library — Custom Hook testlash