React / Testing / RTL

React Testing Library

queries (getByRole, findBy), user-event v14 (type, click, keyboard), MSW mocking, async testing i RTL best practices.

getByRole
Semantic query
user-event
User sim
MSW
API mock
findBy
Async wait

6 queries RTL — priorytety i zastosowanie

getByRole, getByLabelText, getByText, findBy, queryBy i getByTestId — priorytet, przykład i kiedy używać.

QueryPriorytetPrzykładKiedy
getByRole1 (najwyższy)getByRole('button', {name: 'Submit'})Przyciski, linki, inputy, listy — ARIA role
getByLabelText2getByLabelText('Email address')Pola formularza z label
getByText4getByText(/hello/i)Tekst widoczny dla użytkownika
findBy*Asyncawait findByText('Loaded')Async — czeka na pojawienie elementu
queryBy*Assertion nullexpect(queryByText('Error')).not.toBeInTheDocument()Sprawdź brak elementu
getByTestId8 (najniższy)getByTestId('submit-btn')Ostateczność — gdy brak semantycznego selektora

Często zadawane pytania

Co to jest React Testing Library i jak różni się od Enzyme?

React Testing Library (RTL): testuj jak użytkownik, nie jak implementacja. Kent C. Dodds. Brak testowania internals (state, instance methods). Testuj: co widzi użytkownik. Co może zrobić. Co dostępne przez ARIA. Instalacja: npm install @testing-library/react @testing-library/user-event @testing-library/jest-dom --save-dev. Setup Vitest: import '@testing-library/jest-dom'. importMetaGlobEager lub setupFiles w vitest.config. Render: import {render, screen} from '@testing-library/react'. render(Button label='Click me' onClick={fn}). Queries: screen.getByText('Click me') — throw jeśli nie znajdzie. screen.queryByText('...') — null jeśli brak. screen.findByText('...') — async, czeka. getByRole('button', {name: 'Click me'}) — ARIA. getByLabelText('Email') — form labels. getByPlaceholderText('...'). getByTestId('...') — ostateczność. Hierarchy: getBy (1, throw), queryBy (0-1, null), findBy (async). getAllBy (1+), queryAllBy (0+), findAllBy (async 1+). Enzyme (stary sposób): testuje implementację. shallow rendering. enzyme.find('.button'). wrapper.state(). Nie polecany — problemy z React 18+. RTL preferowany w 2024. Enzyme vs RTL filozofia: Enzyme — testuj jak deweloper. RTL — testuj jak użytkownik. RTL bardziej dostosowane do refactoringu.

user-event — symulowanie interakcji użytkownika?

@testing-library/user-event: realistyczne simulowanie zdarzeń. Bardziej realistyczne niż fireEvent. Sekwencja zdarzeń jak prawdziwy browser. Instalacja: npm install @testing-library/user-event --save-dev. Setup: import userEvent from '@testing-library/user-event'. const user = userEvent.setup(). Await interakcje. Kliknięcie: await user.click(screen.getByRole('button')). Wpisywanie: await user.type(screen.getByRole('textbox'), 'Hello {enter}'). Specjalne klawisze: {enter}, {tab}, {escape}, {backspace}, {delete}, {arrowup}. Zaznaczenie i clipboard: await user.selectAll(input). await user.copy(). await user.paste(). Hover: await user.hover(element). await user.unhover(element). Keyboard: await user.keyboard('{Tab}Hello{Enter}'). Drag: user.pointer. fireEvent vs user-event: fireEvent.click() — jeden event. userEvent.click() — pełna sekwencja (pointerover, mouseenter, pointermove, mousemove, pointerdown, mousedown, pointerup, mouseup, click). Bardziej realistyczny. Formularz test: render(ContactForm). const user = userEvent.setup(). await user.type(screen.getByLabelText('Email'), 'test@example.com'). await user.type(screen.getByLabelText('Message'), 'Hello'). await user.click(screen.getByRole('button', {name: 'Send'})). expect(screen.getByText('Message sent!')).toBeInTheDocument(). Timer mocking: jest.useFakeTimers lub Vitest fake timers. user.setup({delay: null}) — brak opóźnień.

Mockowanie w React Testing Library — API, Context i hooks?

Mock funkcji: vi.fn() (Vitest) lub jest.fn(). const onClick = vi.fn(). render(Button onClick={onClick}). await user.click(button). expect(onClick).toHaveBeenCalledTimes(1). Mock API (MSW preferowany): msw + @mswjs/data. handlers.ts: http.get('/api/user', () => HttpResponse.json({id: 1, name: 'Adam'})). server.use(handler) — per-test. Context Provider w testach: const AllProviders = ({children}) => ( QueryClientProvider client={new QueryClient()} ThemeProvider theme='dark' {children} /ThemeProvider /QueryClientProvider ). render(MyComponent, {wrapper: AllProviders}). customRender helper: const customRender = (ui, options) => render(ui, {wrapper: AllProviders, ...options}). Router wrapping: render(ui, {wrapper: MemoryRouter}). lub: BrowserRouter w wrapper. useNavigate mock: vi.mock('react-router-dom', () => ({...vi.importActual('react-router-dom'), useNavigate: () => mockNavigate})). Zustand mock: mockStore stan w teście. @testing-library/react-hooks (teraz renderHook): renderHook(() => useCounter()). const {result} = renderHook(). act(() => result.current.increment()). expect(result.current.count).toBe(1). Wait for: await waitFor(() => expect(screen.getByText('Loaded')).toBeInTheDocument()). async updates po API call. TanStack Query w testach: QueryClient nowy per test. brak cacheTime. dehydrate / hydrate. jest-axe integracja: const results = await axe(container). expect(results).toHaveNoViolations().

Testowanie asynchronicznych komponentów i hooks?

Async komponenty: fetchData na mount. loading -> data -> error. Test loading state: render(UserProfile id='1'). expect(screen.getByText('Loading...')).toBeInTheDocument(). Test data state: await screen.findByText('Adam'). findBy = czeka max 1000ms. waitFor: await waitFor(() => expect(screen.queryByText('Loading...')).not.toBeInTheDocument()). await waitFor(() => { expect(screen.getByText('Error')).toBeInTheDocument() }). Interval / timeout: vi.useFakeTimers(). render(Component). vi.advanceTimersByTime(1000). expect(screen.getByText('Updated')).toBeInTheDocument(). vi.useRealTimers() po teście. TanStack Query async: wrapper z QueryClient. server.use(handler) dla konkretnego testu. await screen.findByRole('list'). Nie await screen.findByText — zbyt szczegółowe. Infinite scroll: user.scroll(container, {deltaY: 500}). lub Intersection Observer mock. useInfiniteQuery testowanie. Form submit async: await user.type(emailInput, 'test@test.pl'). await user.click(submitButton). await screen.findByText('Zarejestrowałeś się!'). Server error: server.use(http.post('/api/register', () => new HttpResponse(null, {status: 500}))). await screen.findByText('Błąd rejestracji'). act() wrapper: automatyczny w RTL. Nie trzeba ręcznie w większości przypadków. Potrzebny przy: zmiana state poza render. Imperativo focus. Custom event. Concurrent mode: wrappuj w act async gdy potrzeba. waitFor lepszy niż act().

Best practices i antypatterns RTL — jak pisać dobre testy?

Dobre praktyki: Testuj z perspektywy użytkownika. Brak testowania implementacji (state, class names). Używaj semantycznych queries (role, label, text). Testuj dostępność razem z RTL. Jedna asercja per test (lub logiczne grupowanie). Nie znasz ścieżki do elementu — używaj aria. getByRole priorytet: role + accessible name. Hierarchia queries: 1. getByRole. 2. getByLabelText. 3. getByPlaceholderText. 4. getByText. 5. getByDisplayValue. 6. getByAltText. 7. getByTitle. 8. getByTestId (ostateczność). Dobre: expect(screen.getByRole('button', {name: /submit/i})).toBeEnabled(). Złe: expect(container.querySelector('.btn-primary')).toExist(). Złe: wrapper.find('Button').props().onClick(). Testuj error states: expect(screen.getByRole('alert')).toHaveTextContent('Invalid email'). Testuj loading: findBy — czeka. Nie getBy z setTimeout. Cleanup: RTL automatyczny cleanup po każdym teście. Nie musisz wywołać cleanup(). Custom queries: buildQueries() dla własnych selektorów. @testing-library/user-event v14+: userEvent.setup() zawsze. Nie stary userEvent.type(el, text). Debugowanie: screen.debug() — wypisz DOM. prettyDOM(element). logRoles(container.querySelector('nav')) — listuj role. Fake timers i async: vi.useFakeTimers() + userEvent.setup({delay: null}). flushPromises() lub await null. Testy izolowane: każdy test = fresh state. beforeEach cleanup. Nie shared state między testami. MSW per test: server.use(overrideHandler) — nadpisuje dla testu. server.resetHandlers() w afterEach.

Kontakt

Skontaktuj się z nami

Porozmawiajmy o Twoim projekcie. Bezpłatna wycena w ciągu 24 godzin.

Wyślij zapytanie

Bezpłatna wycena w 24h
Bez zobowiązań
Indywidualne podejście
Ekspresowa realizacja

Telefon

+48 790 814 814

Pon-Pt: 9:00 - 18:00

Email

adam@fotz.pl

Odpowiadamy w ciągu 24h

Adres rejestrowy

Poznańska 117/11

62-023 Kamionki · obsługa zdalna

Godziny pracy

Pon - Pt9:00 - 18:00
Sob - NdzZamknięte

Wolisz porozmawiać?

Zadzwoń teraz i porozmawiaj z naszym specjalistą o Twoim projekcie.

Zadzwoń teraz