React Testing Library — Custom Hook testlash
Muammodan boshlaylik
Sizda bir nechta custom hook bor (24–25-darslar):
function useHisoblagich(boshlangich = 0, { min = -Infinity, max = Infinity } = {}) {
const [son, setSon] = useState(boshlangich);
const oshir = () => setSon((s) => Math.min(s + 1, max));
const kamaytir = () => setSon((s) => Math.max(s - 1, min));
const tozala = () => setSon(boshlangich);
return { son, oshir, kamaytir, tozala };
}
Uni oddiy funksiya kabi testlab bo'lmaydi:
const natija = useHisoblagich(5); // ❌ Invalid hook call
Hook lar faqat komponent ichida chaqirilishi mumkin. Har bir hook uchun test komponenti yasash — noqulay. RTL buning uchun renderHook ni beradi.
renderHook
import { renderHook, act } from '@testing-library/react';
import { useHisoblagich } from './useHisoblagich';
describe('useHisoblagich', () => {
it('boshlang\'ich qiymatdan boshlaydi', () => {
const { result } = renderHook(() => useHisoblagich(5));
expect(result.current.son).toBe(5);
});
it('oshiradi', () => {
const { result } = renderHook(() => useHisoblagich(0));
act(() => {
result.current.oshir();
});
expect(result.current.son).toBe(1);
});
it('max dan oshmaydi', () => {
const { result } = renderHook(() => useHisoblagich(9, { max: 10 }));
act(() => {
result.current.oshir();
result.current.oshir();
result.current.oshir();
});
expect(result.current.son).toBe(10);
});
});
renderHook(() => useX(...))— hook ni kichik ichki komponentda chaqiradiresult.current— hook ning joriy qaytargan qiymatiact(() => ...)— state ni o'zgartiradigan chaqiruvlarni o'rash; React yangilanishlarni qo'llaydi
result.current — har doim yangi
const { result } = renderHook(() => useHisoblagich(0));
const { son, oshir } = result.current; // ❌ eski qiymatni saqlab qoldik
act(() => oshir());
expect(son).toBe(1); // ❌ 0 — bu render dan oldingi qiymat
expect(result.current.son).toBe(1); // ✅ har safar result.current dan o'qing
result.current har renderdan keyin yangilanadi. Destructuring qilsangiz — o'sha paytdagi "surat" qoladi.
[!INFO] Nima uchun bu yerda
actkerak, 75-darsda esa yo'q edi?userEventvarendero'zlariactga o'ralgan.renderHookda esa hook funksiyalarini to'g'ridan-to'g'ri chaqiramiz — React ga "state o'zgaradi, yangilanishlarni qo'lla" deyish uchunactkerak.
Props ni o'zgartirish — rerender
function useDebounce(qiymat, kechikish) {
const [debounced, setDebounced] = useState(qiymat);
useEffect(() => {
const t = setTimeout(() => setDebounced(qiymat), kechikish);
return () => clearTimeout(t);
}, [qiymat, kechikish]);
return debounced;
}
describe('useDebounce', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('kechikishdan keyin yangi qiymatni qaytaradi', () => {
const { result, rerender } = renderHook(
({ qiymat }) => useDebounce(qiymat, 300),
{ initialProps: { qiymat: 'a' } }
);
expect(result.current).toBe('a');
rerender({ qiymat: 'ab' });
expect(result.current).toBe('a'); // hali eski
act(() => {
vi.advanceTimersByTime(299);
});
expect(result.current).toBe('a'); // hali ham
act(() => {
vi.advanceTimersByTime(1);
});
expect(result.current).toBe('ab'); // ✅ 300 ms o'tdi
});
it('tez o\'zgarishlarda faqat oxirgisini qaytaradi', () => {
const { result, rerender } = renderHook(
({ qiymat }) => useDebounce(qiymat, 300),
{ initialProps: { qiymat: 'r' } }
);
rerender({ qiymat: 're' });
act(() => vi.advanceTimersByTime(100));
rerender({ qiymat: 'rea' });
act(() => vi.advanceTimersByTime(100));
rerender({ qiymat: 'react' });
act(() => vi.advanceTimersByTime(300));
expect(result.current).toBe('react');
});
});
initialProps— birinchi render props irerender(yangiProps)— hook ni yangi argumentlar bilan qayta chaqirish (komponent props i o'zgargandek)
unmount — cleanup ni tekshirish
it('unmount da taymer tozalanadi', () => {
const clearSpy = vi.spyOn(globalThis, 'clearTimeout');
const { unmount } = renderHook(() => useDebounce('a', 300));
unmount();
expect(clearSpy).toHaveBeenCalled();
});
Async hook lar
function useFoydalanuvchi(id) {
return useQuery({
queryKey: ['user', id],
queryFn: () => getUser(id),
});
}
TanStack Query ishlatadigan hook ga QueryClientProvider kerak — wrapper orqali:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
vi.mock('../api/users');
it('foydalanuvchini yuklaydi', async () => {
usersApi.getUser.mockResolvedValue({ id: 1, ism: 'Ali' });
const { result } = renderHook(() => useFoydalanuvchi(1), {
wrapper: createWrapper(),
});
expect(result.current.isPending).toBe(true);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({ id: 1, ism: 'Ali' });
});
it('xatoni qaytaradi', async () => {
usersApi.getUser.mockRejectedValue(new Error('404'));
const { result } = renderHook(() => useFoydalanuvchi(999), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error.message).toBe('404');
});
wrapper— hook ni o'raydigan Provider komponenti (Context, Router, Query, Redux)- Async natija —
waitFor(76-dars) retry: false— aks holda xato testi 3 marta qayta urinishni kutadi va sekinlashadi
Context ishlatadigan hook
const wrapper = ({ children }) => (
<ThemeProvider boshlangich="qorongi">{children}</ThemeProvider>
);
const { result } = renderHook(() => useTheme(), { wrapper });
expect(result.current.mavzu).toBe('qorongi');
Zustand store ni testlash
Zustand store — React dan tashqarida ham ishlaydi (57-dars), shuning uchun ko'pincha renderHook ham kerak emas:
import { useSavatStore } from './savatStore';
const boshlangich = useSavatStore.getState();
beforeEach(() => {
useSavatStore.setState(boshlangich, true); // har testdan oldin toza holat
});
it('mahsulot qo\'shadi', () => {
useSavatStore.getState().qoshish({ id: 1, nomi: 'Kitob', narx: 50000 });
expect(useSavatStore.getState().mahsulotlar).toHaveLength(1);
});
it('bir xil mahsulotning sonini oshiradi', () => {
const { qoshish } = useSavatStore.getState();
qoshish({ id: 1, nomi: 'Kitob', narx: 50000 });
qoshish({ id: 1, nomi: 'Kitob', narx: 50000 });
expect(useSavatStore.getState().mahsulotlar[0].soni).toBe(2);
});
[!WARNING] Zustand store — modul darajasidagi global obyekt: testlar orasida holat saqlanib qoladi.
beforeEachda tozalamasangiz, bir test boshqasiga ta'sir qiladi va natija testlar tartibiga bog'liq bo'lib qoladi. Redux store uchun esa har testdaconfigureStorebilan yangi store yarating.
Hook ni alohida yoki komponent orqali?
| Holat | Tavsiya |
|---|---|
Hook — qayta ishlatiladigan kutubxona kabi (useDebounce, useLocalStorage) |
renderHook bilan alohida |
| Hook da murakkab mantiq (hisob-kitob, holat mashinasi) | renderHook bilan alohida |
| Hook faqat bitta komponentda, oddiy | Komponent testi orqali — hook avtomatik tekshiriladi |
useQuery ning oddiy o'rami |
Ko'pincha komponent testi yetarli |
[!TIP]
renderHook— hook ning API si ni tekshiradi; komponent testi — foydalanuvchi natijasi ni. Agar hook faqat bitta komponentning ichki tafsiloti bo'lsa, uni alohida testlash — implementatsiyani testlash (73-dars): hook ni komponentga qo'shib yuborsangiz, test buziladi, garchi hech narsa o'zgarmagan bo'lsa ham.
Nima uchun shunday?
Nima uchun hook larni oddiy funksiya kabi chaqirib bo'lmaydi — ular ham funksiya-ku?
Chunki hook lar React ning ichki holatiga bog'liq. useState qiymatni funksiyaning o'zida emas, React "hozir render bo'layotgan komponent" uchun saqlaydigan maxsus ro'yxatda saqlaydi — va uni chaqirilish tartibi bo'yicha topadi (hook qoidalari aynan shundan kelib chiqadi). Komponentdan tashqarida "hozirgi komponent" yo'q — React qiymatni qayerga yozishni bilmaydi va xato beradi. renderHook bu muammoni oddiy hiyla bilan hal qiladi: u hook ni chaqiradigan kichik, ko'rinmas test komponentini yaratadi va natijani result.current ga yozib turadi. Siz esa hook bilan xuddi uning API si orqali ishlaysiz.
Xulosa
Eslab qoling:
renderHook(() => useX(args))— hook ni test komponentida chaqiradiresult.current— har doim shu yerdan o'qing; destructuring qilmang- State ni o'zgartiruvchi chaqiruvlar —
act(() => ...)ichida initialProps+rerender(props)— argumentlar o'zgarishi;unmount()— cleanup- Provider lar —
wrapperorqali; Query uchun har testda yangiQueryClient,retry: false - Async natija —
waitFor - Zustand —
getState()bilan to'g'ridan-to'g'ri;beforeEachda holatni tiklash - Qayta ishlatiladigan va murakkab hook lar — alohida; oddiylar — komponent orqali
Keyingi dars
78-dars: MSW — API mock qilish, real so'rovlarsiz test