React Testing Library — render, screen, userEvent

~20 daq · React · Testing

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 getByRole elementni 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 — await ni unutmang
  • userEvent haqiqiy foydalanuvchini to'liq taqlid qiladi: click dan oldin pointerdown, mousedown, focus, keyin mouseup, click. disabled tugmani bosib bo'lmaydi — xuddi haqiqatdagidek

[!INFO] fireEvent — eskiroq va pastroq darajadagi vosita: u faqat bitta hodisani yuboradi (fireEvent.click — faqat click). userEvent — haqiqiy foydalanuvchiga yaqinroq va tavsiya etiladi. fireEvent ni faqat userEvent qila 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 />) → screen bilan qidirish → userEvent bilan harakat → expect bilan tekshirish
  • Query ustuvorligi: getByRole → getByLabelText → getByText → ... → getByTestId (oxirgi chora)
  • getBy — bor bo'lishi kerak; queryBy — yo'qligini tekshirish; findBy — async paydo bo'lish
  • const user = userEvent.setup(); har bir amal — await
  • jest-dom: toBeInTheDocument, toHaveValue, toBeDisabled, toHaveTextContent
  • Provider lar — renderWithProviders; har testda yangi QueryClient (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