| name | react-test |
| description | React and testing patterns |
Kent C. Dodds Patterns
Apply Kent C. Dodds' philosophy and patterns for React development and testing.
Core Philosophy
The Testing Trophy (not Pyramid)
╱╲
╱ ╲ E2E (few)
╱────╲
╱ ╲ Integration (most)
╱────────╲
╱ ╲ Unit (some)
╱────────────╲
╱ ╲ Static (ESLint, TypeScript)
╱────────────────╲
Key insight: Integration tests give the best confidence-to-effort ratio.
The Golden Rule
"Write tests. Not too many. Mostly integration."
Test Like Users
"The more your tests resemble the way your software is used, the more confidence they can give you."
Testing Patterns
1. Query Priority (in order of preference)
getByRole('button', { name: /submit/i })
getByLabelText('Email')
getByPlaceholderText('Enter email')
getByText('Welcome')
getByAltText('Profile picture')
getByTitle('Close')
getByTestId('submit-button')
Never use: container.querySelector, DOM structure queries
2. User Event Over fireEvent
fireEvent.click(button)
fireEvent.change(input, { target: { value: 'text' } })
import userEvent from '@testing-library/user-event'
const user = userEvent.setup()
await user.click(button)
await user.type(input, 'text')
3. Avoid Implementation Details
expect(component.state.isOpen).toBe(true)
expect(wrapper.find('Modal').props().visible).toBe(true)
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByText('Modal content')).toBeVisible()
4. One Assertion Per Behavior (not per test)
test('submitting the form shows success message', async () => {
const user = userEvent.setup()
render(<ContactForm />)
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
await user.type(screen.getByLabelText(/message/i), 'Hello')
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(screen.getByRole('alert')).toHaveTextContent(/success/i)
expect(screen.queryByLabelText(/email/i)).not.toBeInTheDocument()
})
5. Avoid Cleanup That Hides Bugs
afterEach(() => {
jest.clearAllMocks()
cleanup()
})
6. Test Error States
test('shows error when submission fails', async () => {
server.use(
rest.post('/api/contact', (req, res, ctx) => {
return res(ctx.status(500))
})
)
const user = userEvent.setup()
render(<ContactForm />)
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(screen.getByRole('alert')).toHaveTextContent(/error/i)
})
React Component Patterns
1. Compound Components
<Menu items={items} onSelect={onSelect} renderItem={renderItem} />
<Menu>
<Menu.Button>Options</Menu.Button>
<Menu.List>
<Menu.Item onSelect={() => {}}>Edit</Menu.Item>
<Menu.Item onSelect={() => {}}>Delete</Menu.Item>
</Menu.List>
</Menu>
2. Prop Collections and Getters
function useToggle() {
const [on, setOn] = useState(false)
const toggle = () => setOn(o => !o)
const getTogglerProps = (props = {}) => ({
'aria-pressed': on,
onClick: () => {
props.onClick?.()
toggle()
},
...props,
})
return { on, toggle, getTogglerProps }
}
const { on, getTogglerProps } = useToggle()
<button {...getTogglerProps({ onClick: customHandler })}>
{on ? 'ON' : 'OFF'}
</button>
3. State Reducer Pattern
function useToggle({ reducer = (state, action) => action.changes } = {}) {
const [{ on }, dispatch] = useReducer(
(state, action) => reducer(state, { ...action, changes: toggleReducer(state, action) }),
{ on: false }
)
const toggle = () => dispatch({ type: 'toggle' })
return { on, toggle }
}
const { on, toggle } = useToggle({
reducer: (state, action) => {
if (action.type === 'toggle' && clickedTooMany) {
return state
}
return action.changes
}
})
4. Control Props
function useToggle({ on: controlledOn, onChange } = {}) {
const [internalOn, setInternalOn] = useState(false)
const isControlled = controlledOn !== undefined
const on = isControlled ? controlledOn : internalOn
const toggle = () => {
if (!isControlled) {
setInternalOn(o => !o)
}
onChange?.(!on)
}
return { on, toggle }
}
Accessibility Patterns
1. Semantic HTML First
<div onClick={handleClick}>Click me</div>
<button onClick={handleClick}>Click me</button>
2. ARIA When Needed
<div
role="button"
tabIndex={0}
aria-pressed={isActive}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Toggle
</div>
3. Labels for Inputs
<input placeholder="Email" />
<label>
Email
<input type="email" />
</label>
<label htmlFor="email">Email</label>
<input id="email" type="email" />
<label htmlFor="search" className="sr-only">Search</label>
<input id="search" placeholder="Search..." />
4. Focus Management
function Modal({ isOpen, onClose, children }) {
const closeButtonRef = useRef()
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus()
}
}, [isOpen])
}
Code Review Checklist
When reviewing React code, check:
Testing
Components
Patterns
Quick Reference
| Instead of | Use |
|---|
fireEvent.click() | userEvent.click() |
getByTestId() | getByRole() or getByLabelText() |
wrapper.find() | screen.getByRole() |
expect(state) | expect(screen.getBy...) |
<div onClick> | <button onClick> |
| Mock everything | Mock at network boundary |
Resources