React Testing Library — render, screen, userEvent
Muammodan boshlaylik
74-darsda oddiy funksiyalarni testladik. Lekin React ilovasining asosiy qismi — komponentlar:
function Hisoblagich() {
const [son, setSon] = useState(0);
return (
<div>
<p>Qiymat: {son}</p>
<button onClick={() => setSon(son + 1)}>Oshirish</button>
</div>
);
}
Qanday tekshiramiz? Komponentni chaqirib, qaytgan JSX obyektini solishtiramizmi? son state ini qandaydir yo'l bilan o'qiymizmi?
React Testing Library (RTL) boshqacha yondashadi: komponentni haqiqiy DOM ga (jsdom) render qiladi va siz u bilan foydalanuvchi kabi ishlaysiz — matnni topasiz, tugmani bosasiz, natijani ko'rasiz.
Birinchi komponent testi
// Hisoblagich.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Hisoblagich from './Hisoblagich';
describe('Hisoblagich', () => {
it('boshida 0 ni ko\'rsatadi', () => {
render(<Hisoblagich />);
expect(screen.getByText('Qiymat: 0')).toBeInTheDocument();
});
it('tugma bosilganda 1 ga oshadi', async () => {
const user = userEvent.setup();
render(<Hisoblagich />);
await user.click(screen.getByRole('button', { name: 'Oshirish' }));
expect(screen.getByText('Qiymat: 1')).toBeInTheDocument();
});
});
Uch asosiy vosita:
| Vosita | Vazifasi |
|---|---|
render(<Komponent />) |
Komponentni DOM ga chiqarish |
screen |
Sahifadan elementlarni qidirish |
userEvent |
Foydalanuvchi harakatlari: bosish, yozish, Tab |
E'tibor bering: test useState haqida hech narsa bilmaydi. Ertaga siz useReducer yoki Zustand ga o'tsangiz — test o'zgarmaydi (73-dars).
Elementlarni qidirish — query lar
getByRole — birinchi tanlov
screen.getByRole('button', { name: 'Saqlash' });
screen.getByRole('heading', { name: 'Savat', level: 1 });
screen.getByRole('textbox', { name: 'Email' });
screen.getByRole('checkbox', { name: /shartlarga roziman/i });
screen.getByRole('link', { name: 'Kurslar' });
screen.getByRole('combobox', { name: 'Viloyat' }); // <select>
screen.getByRole('listitem'); // <li>
screen.getByRole('alert'); // role="alert"
getByRole elementni roli (u nima) va accessible name i (ekran o'quvchi nima deb o'qiydi) bo'yicha topadi. Bu — foydalanuvchi va ekran o'quvchi ko'radigan narsa.
| HTML | Rol |
|---|---|
<button> |
button |
<a href> |
link |
<h1>–<h6> |
heading |
<input type="text">, <textarea> |
textbox |
<input type="checkbox"> |
checkbox |
<input type="radio"> |
radio |
<select> |
combobox |
<ul>, <li> |
list, listitem |
<img alt="..."> |
img |
<nav>, <main> |
navigation, main |
[!TIP] Bonus: agar
getByRoleelementni topa olmasa — ko'pincha accessibility muammosi bor.<div onClick>tugma rolini olmaydi; label siz input nomga ega emas (HTML kursi, 20–21-darslar). RTL testlari shu tariqa accessibility ni ham tekshiradi.
Boshqa query lar — ustuvorlik tartibida
screen.getByLabelText('Email'); // label bilan bog'langan input
screen.getByPlaceholderText('Qidirish...'); // label yo'q bo'lsa (yaxshi emas)
screen.getByText('Savat bo\'sh'); // oddiy matn
screen.getByDisplayValue('Ali'); // input dagi joriy qiymat
screen.getByAltText('Logotip'); // rasm
screen.getByTitle('Yopish');
screen.getByTestId('savat-jami'); // data-testid — oxirgi chora
getByTestId — faqat boshqa hech narsa ishlamaganda. Foydalanuvchi data-testid ni ko'rmaydi.
get, query, find
| Prefiks | Topilmasa | Bir nechta topilsa | Async |
|---|---|---|---|
getBy... |
❌ Xato | ❌ Xato | Yo'q |
queryBy... |
null |
❌ Xato | Yo'q |
findBy... |
❌ Xato (kutib bo'lgach) | ❌ Xato | ✅ Promise — paydo bo'lishini kutadi |
getAllBy... / queryAllBy... / findAllBy... |
Massiv |
// Bor bo'lishi kerak
expect(screen.getByRole('button')).toBeInTheDocument();
// YO'Q bo'lishi kerak — queryBy (getBy xato beradi)
expect(screen.queryByText('Xato')).not.toBeInTheDocument();
// Keyinroq paydo bo'ladi (API javobidan keyin) — findBy (76-dars)
expect(await screen.findByText('Ali Valiyev')).toBeInTheDocument();
// Bir nechta
expect(screen.getAllByRole('listitem')).toHaveLength(3);
userEvent — foydalanuvchi harakatlari
const user = userEvent.setup(); // har testning boshida
await user.click(tugma);
await user.dblClick(element);
await user.type(input, 'Salom'); // harfma-harf yozadi
await user.clear(input);
await user.selectOptions(select, 'samarqand');
await user.tab(); // Tab tugmasi
await user.keyboard('{Enter}');
await user.hover(element);
await user.upload(faylInput, new File(['...'], 'chek.png', { type: 'image/png' }));
- Har bir amal async —
awaitni unutmang userEventhaqiqiy foydalanuvchini to'liq taqlid qiladi:clickdan oldinpointerdown,mousedown,focus, keyinmouseup,click.disabledtugmani bosib bo'lmaydi — xuddi haqiqatdagidek
[!INFO]
fireEvent— eskiroq va pastroq darajadagi vosita: u faqat bitta hodisani yuboradi (fireEvent.click— faqatclick).userEvent— haqiqiy foydalanuvchiga yaqinroq va tavsiya etiladi.fireEventni faqatuserEventqila olmaydigan hollarda ishlating (masalan,scroll).
jest-dom tekshiruvlari
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(tugma).toBeDisabled();
expect(tugma).toBeEnabled();
expect(input).toHaveValue('Ali');
expect(checkbox).toBeChecked();
expect(element).toHaveTextContent('Jami: 150 000');
expect(element).toHaveClass('faol');
expect(input).toHaveFocus();
expect(input).toBeInvalid(); // aria-invalid yoki :invalid
expect(link).toHaveAttribute('href', '/savat');
expect(input).toHaveAccessibleDescription('Kamida 8 belgi');
Forma testi — to'liq misol
// Login.jsx
function Login({ onKirish }) {
const [email, setEmail] = useState('');
const [parol, setParol] = useState('');
const [xato, setXato] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (parol.length < 8) {
setXato('Parol kamida 8 belgi');
return;
}
onKirish({ email, parol });
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<label htmlFor="parol">Parol</label>
<input id="parol" type="password" value={parol} onChange={(e) => setParol(e.target.value)} />
{xato && <p role="alert">{xato}</p>}
<button type="submit">Kirish</button>
</form>
);
}
// Login.test.jsx
describe('Login', () => {
it("to'g'ri ma'lumot bilan onKirish ni chaqiradi", async () => {
const user = userEvent.setup();
const onKirish = vi.fn();
render(<Login onKirish={onKirish} />);
await user.type(screen.getByLabelText('Email'), 'ali@mail.uz');
await user.type(screen.getByLabelText('Parol'), 'maxfiy123');
await user.click(screen.getByRole('button', { name: 'Kirish' }));
expect(onKirish).toHaveBeenCalledWith({ email: 'ali@mail.uz', parol: 'maxfiy123' });
});
it('qisqa parolda xato ko\'rsatadi va yubormaydi', async () => {
const user = userEvent.setup();
const onKirish = vi.fn();
render(<Login onKirish={onKirish} />);
await user.type(screen.getByLabelText('Parol'), '123');
await user.click(screen.getByRole('button', { name: 'Kirish' }));
expect(screen.getByRole('alert')).toHaveTextContent('Parol kamida 8 belgi');
expect(onKirish).not.toHaveBeenCalled();
});
it('Enter bilan ham yuboriladi', async () => {
const user = userEvent.setup();
const onKirish = vi.fn();
render(<Login onKirish={onKirish} />);
await user.type(screen.getByLabelText('Parol'), 'maxfiy123{Enter}');
expect(onKirish).toHaveBeenCalled();
});
});
Provider lar bilan render
Komponent Router, TanStack Query yoki Redux ga muhtoj bo'lsa — ularni o'rash kerak:
// src/test/utils.jsx
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export function renderWithProviders(ui, { route = '/' } = {}) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, // testda qayta urinish yo'q
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>
</QueryClientProvider>
);
}
renderWithProviders(<MahsulotSahifasi />, { route: '/mahsulot/42' });
Har bir testda yangi QueryClient — testlar orasida kesh aralashmasin.
Debug
screen.debug(); // joriy DOM ni konsolga chiqaradi
screen.debug(screen.getByRole('form')); // faqat bir qismi
screen.logTestingPlaygroundURL(); // brauzerda query tanlash vositasi
Element topilmasa, RTL xato xabarida mavjud rollar ro'yxatini ham ko'rsatadi — ko'pincha shuning o'zi muammoni tushuntiradi.
Nima uchun shunday?
Nima uchun RTL komponentning state i yoki props lariga to'g'ridan-to'g'ri kirishga imkon bermaydi?
Bu atayin qilingan cheklov. Avvalgi mashhur vosita — Enzyme — komponentning ichiga kirish imkonini berardi: wrapper.state('son'), wrapper.instance().metod(). Natijada testlar implementatsiyaga bog'lanib qolardi: class komponentni hook larga o'tkazish yoki state ni boshqa joyga ko'chirish yuzlab testlarni buzardi — ilova esa to'g'ri ishlayotgan bo'lsa ham. RTL faqat foydalanuvchi ko'radigan va qila oladigan narsalarni beradi: matn, rollar, label lar, bosish, yozish. Bu testlarni yozishni biroz qiyinlashtiradi, lekin ularni qimmatli qiladi: test yiqilsa — demak foydalanuvchi uchun nimadir haqiqatan buzilgan.
Xulosa
Eslab qoling:
render(<K />)→screenbilan qidirish →userEventbilan harakat →expectbilan tekshirish- Query ustuvorligi:
getByRole→getByLabelText→getByText→ ... →getByTestId(oxirgi chora) getBy— bor bo'lishi kerak;queryBy— yo'qligini tekshirish;findBy— async paydo bo'lishconst user = userEvent.setup(); har bir amal —await- jest-dom:
toBeInTheDocument,toHaveValue,toBeDisabled,toHaveTextContent - Provider lar —
renderWithProviders; har testda yangiQueryClient(retry: false) screen.debug()— joriy DOM ni ko'rish- Implementatsiyani emas, foydalanuvchi ko'radigan natijani tekshiring
Keyingi dars
76-dars: React Testing Library — async testlar, mock