React Testing Library
queries (getByRole, findBy), user-event v14 (type, click, keyboard), MSW mocking, async testing i RTL best practices.
6 queries RTL — priorytety i zastosowanie
getByRole, getByLabelText, getByText, findBy, queryBy i getByTestId — priorytet, przykład i kiedy używać.
| Query | Priorytet | Przykład | Kiedy |
|---|---|---|---|
| getByRole | 1 (najwyższy) | getByRole('button', {name: 'Submit'}) | Przyciski, linki, inputy, listy — ARIA role |
| getByLabelText | 2 | getByLabelText('Email address') | Pola formularza z label |
| getByText | 4 | getByText(/hello/i) | Tekst widoczny dla użytkownika |
| findBy* | Async | await findByText('Loaded') | Async — czeka na pojawienie elementu |
| queryBy* | Assertion null | expect(queryByText('Error')).not.toBeInTheDocument() | Sprawdź brak elementu |
| getByTestId | 8 (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.
Powiązane artykuły
Skontaktuj się z nami
Porozmawiajmy o Twoim projekcie. Bezpłatna wycena w ciągu 24 godzin.
Wyślij zapytanie
Telefon
+48 790 814 814
Pon-Pt: 9:00 - 18:00
adam@fotz.pl
Odpowiadamy w ciągu 24h
Adres
Plac Wolności 16
61-739 Poznań
Godziny pracy
Wolisz porozmawiać?
Zadzwoń teraz i porozmawiaj z naszym specjalistą o Twoim projekcie.
Zadzwoń teraz