๐Ÿณ IntuitiveFE
Login
โ† All concepts

Testing React: RTL & MSW

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐ŸŽฐ RTL testing = testing a vending machine as a customer (press button, get snack) โ€” not as an engineer (check internal gears). Test what users see and do, not implementation details.

Implementation testing (fragile)

Query by CSS class, check internal state, mock module internals, assert function call signatures. Breaks on refactor even when behavior is unchanged.

js
1querySelector('.btn-primary')
2expect(component.state.count)
3jest.mock('./useCounter')

Behavior testing (durable)

Query by ARIA role, simulate real user events, mock at the network boundary (MSW). Survives refactors that don't change observable behavior.

js
1getByRole('button')
2await user.click(...)
3msw: http.get('/api/...')

The query priority (highest to lowest)

1. getByRole โ€” ARIA role + accessible name. Always prefer.

2. getByLabelText โ€” for form inputs with labels.

3. getByPlaceholderText โ€” when there's no label.

4. getByText โ€” for non-interactive text content.

5. getByDisplayValue โ€” for current value of select/input.

6. getByTitle โ€” last resort for title attribute.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Key queries

getByRole, getByLabelText, getByText

Render & Query โ€” anti-pattern
js
1// โœ— Query by implementation detail โ€” fragile tests
2const { container } = render(<Button />);
3const btn = container.querySelector('.btn-primary'); // CSS class
4const btn2 = container.querySelector('[data-testid="submit"]'); // test ID
5ย 
6// Problems:
7// 1. Refactor CSS class โ†’ test breaks (but feature still works)
8// 2. data-testid = production code written for tests (smell)
9// 3. Tests what developers see, not what users see
Gotcha: getByRole queries the accessible role โ€” `<div>` has no role, `<button>` has role='button', `<a href>` has role='link'. If your component has the wrong semantic HTML, getByRole forces you to fix it.
Insight: The Testing Library query priority is a ranking of accessibility. Tests that use getByRole are by definition testing what screen reader users experience โ€” accessibility comes for free.
Explored:๐Ÿ”๐Ÿ–ฑ๏ธโฑ๏ธ๐Ÿ•ต๏ธ๐Ÿช
๐ŸŽฏ

Challenge

Toggle between bad and good code for all 5 patterns. Understand what each anti-pattern gets wrong.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: Why should you not query by CSS class or test ID?

CSS classes are implementation details. Renaming a class for a design refactor breaks tests, even though the feature still works for users. Test IDs (data-testid) add production attributes that have no semantic meaning โ€” it's test-pollution in production HTML. ARIA roles test what users and screen readers actually experience: the semantic meaning, not the styling.

Q: What is the difference between getBy, queryBy, and findBy?

js
1// getBy* โ€” throws if not found, throws if multiple found
2const btn = screen.getByRole('button'); // synchronous
3ย 
4// queryBy* โ€” returns null if not found (use for absence checks)
5const error = screen.queryByText(/error/i);
6expect(error).not.toBeInTheDocument(); // test it's absent
7ย 
8// findBy* โ€” returns Promise, polls until found (or timeout)
9const name = await screen.findByText('Alice'); // async
10// Use after async operations (fetch, timer, state update)

Q: Why does RTL recommend userEvent over fireEvent?

fireEvent dispatches a single browser event. Real users trigger a sequence: typing fires focus + multiple keydown/keypress/keyup/input/change events. Clicking fires mouseover + mouseenter + mousemove + mousedown + focus + mouseup + click. Components that rely on any of these events (e.g., onBlur validation, pointer-based disabled logic) behave incorrectly when tested with fireEvent. userEvent simulates the full sequence.

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Testing Library philosophy

Kent C. Dodds' core principle: "The more your tests resemble the way your software is used, the more confidence they can give you." Tests that query by ARIA role, simulate real user events, and mock at the network layer are the most likely to catch real bugs and the least likely to break on refactors.

Testing portals and context-wrapped components

js
1// Testing a component that uses React portals
2// Portal renders outside the component tree โ€” screen still finds it
3render(<Modal />);
4await user.click(screen.getByRole('button', { name: /open/i }));
5// Modal content renders in document.body via portal
6expect(screen.getByRole('dialog')).toBeInTheDocument();
7ย 
8// Testing a component that needs context
9function renderWithProviders(ui: ReactElement) {
10 return render(
11 <QueryClientProvider client={queryClient}>
12 <ThemeProvider>
13 {ui}
14 </ThemeProvider>
15 </QueryClientProvider>
16 );
17}
18// Use everywhere: renderWithProviders(<UserCard userId="1" />)

Playwright component testing

Playwright now supports component testing (`@playwright/experimental-ct-react`). Components render in a real browser context โ€” Chromium, Firefox, or WebKit. This catches browser-specific bugs that jsdom misses (CSS layout, native form behavior, Web APIs). The trade-off: slower than jsdom. Use Playwright component tests for components with complex browser-dependent behavior; use RTL for everything else.

Accessibility testing with @testing-library/jest-dom

js
1import '@testing-library/jest-dom';
2ย 
3// Custom matchers from jest-dom
4expect(btn).toBeInTheDocument();
5expect(btn).toBeVisible();
6expect(btn).toBeEnabled();
7expect(btn).toHaveFocus();
8expect(btn).toHaveAccessibleName('Submit Order');
9expect(input).toHaveValue('test@email.com');
10expect(form).toHaveFormValues({ email: 'test@email.com' });
11ย 
12// Accessibility violation check with jest-axe
13import { axe, toHaveNoViolations } from 'jest-axe';
14expect.extend(toHaveNoViolations);
15ย 
16test('has no accessibility violations', async () => {
17 const { container } = render(<Form />);
18 const results = await axe(container);
19 expect(results).toHaveNoViolations();
20});
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

getByRole tests what screen readers and users actually experience โ€” ARIA semantics โ€” not implementation details.

getBy throws if missing (sync); queryBy returns null if missing (sync); findBy returns a Promise and waits.

userEvent fires the full browser event sequence; fireEvent fires a single event, missing blur, keydown, pointer events that real components rely on.

MSW intercepts at the network layer โ€” your component makes real fetch() calls; MSW intercepts them like a real server would.

renderHook mounts the hook in a minimal React component; you call hook methods inside act() and assert on result.current.

A wrapper function that renders any component inside all required Providers โ€” prevents Provider-missing errors in tests for context-dependent components.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What role does `screen` play in modern Testing Library usage?