| name | dodds-testing-practices |
| description | Write JavaScript code in the style of Kent C. Dodds, testing advocate and React educator. Emphasizes testing best practices, React patterns, and developer productivity. Use when writing tests or building maintainable React applications. |
| tags | testing, react, javascript, integration-testing, test-library, accessibility, best-practices, frontend, automation |
Kent C. Dodds Style Guide
Overview
Kent C. Dodds is a testing advocate, educator, and creator of Testing Library. His philosophy centers on writing tests that give confidence, avoiding implementation details, and making React code maintainable.
Core Philosophy
"The more your tests resemble the way your software is used, the more confidence they can give you."
"Write tests. Not too many. Mostly integration."
"Avoid testing implementation details."
Dodds believes tests should focus on user behavior, not internal mechanics, and that fewer well-written tests beat many brittle ones.
Design Principles
-
Test User Behavior: Test what users see and do, not how code works internally.
-
Confidence Over Coverage: Tests should give confidence, not just increase metrics.
-
Integration Over Unit: Integration tests give the best ROI.
-
Avoid Implementation Details: Tests shouldn't break when refactoring.
When Writing Code
Always
- Query elements the way users find them (by role, label, text)
- Test user flows, not individual functions
- Use realistic data in tests
- Make tests independent and isolated
- Write accessible components (they're easier to test!)
- Prefer integration tests over unit tests for UI
Never
- Test implementation details (internal state, method names)
- Use test IDs when semantic queries work
- Mock everything—use real components when possible
- Write tests that break on refactoring
- Snapshot test entire components
- Test third-party libraries
Prefer
getByRole over getByTestId
userEvent over fireEvent
- Real network calls in integration tests (with MSW)
- Factories over fixtures
- Async assertions over arbitrary waits
Code Patterns
Testing Library Queries
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('heading', { level: 1 });
screen.getByLabelText(/password/i);
screen.getByText(/welcome back/i);
screen.getByPlaceholderText(/search/i);
screen.getByAltText(/profile photo/i);
screen.getByTestId('complex-chart');
container.querySelector('.submit-btn');
wrapper.find('SubmitButton');
screen.getByTestId('submit');
Testing User Interactions
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('allows users to submit the form', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
await user.type(
screen.getByRole('textbox', { name: /name/i }),
'Alice Smith'
);
await user.type(
screen.getByRole('textbox', { name: /email/i }),
'alice@example.com'
);
await user.type(
screen.getByRole('textbox', { name: /message/i }),
'Hello there!'
);
await user.click(screen.getByRole('button', { name: /send/i }));
(onSubmit).({
: ,
: ,
:
});
});
(, {
{ container } = ();
input = container.();
fireEvent.(input, { : { : } });
(wrapper.()).();
});
Async Testing
import { render, screen, waitFor } from '@testing-library/react';
test('loads and displays user data', async () => {
render(<UserProfile userId="123" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const userName = await screen.findByRole('heading', { name: /alice/i });
expect(userName).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/alice@example.com/i)).toBeInTheDocument();
expect(screen.getByRole('img', { name: /avatar/i })).toBeInTheDocument();
});
});
await new Promise(r => setTimeout(r, 1000));
Mocking with MSW
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/user/:id', (req, res, ctx) => {
return res(ctx.json({
id: req.params.id,
name: 'Alice',
email: 'alice@example.com'
}));
}),
rest.post('/api/login', async (req, res, ctx) => {
const { email, password } = await req.json();
if (password === 'correct') {
return res(ctx.json({ token: 'fake-token' }));
}
return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
( server.());
(, () => {
user = userEvent.();
();
user.(screen.(), );
user.(screen.(), );
user.(screen.(, { : }));
( screen.()).();
});
Custom Render Functions
import { render } from '@testing-library/react';
import { ThemeProvider } from './theme';
import { UserProvider } from './user-context';
import { BrowserRouter } from 'react-router-dom';
function AllProviders({ children }) {
return (
<BrowserRouter>
<ThemeProvider>
<UserProvider>
{children}
</UserProvider>
</ThemeProvider>
</BrowserRouter>
);
}
function customRender(ui, options) {
return render(ui, { wrapper: AllProviders, ...options });
}
export * from '@testing-library/react';
export { customRender as render };
import { render, screen } from './test-utils';
test('shows user dashboard', {
();
});
The Testing Trophy
test('formatCurrency formats correctly', () => {
expect(formatCurrency(1234.5)).toBe('$1,234.50');
expect(formatCurrency(0)).toBe('$0.00');
expect(formatCurrency(-50)).toBe('-$50.00');
});
test('user can add item to cart', async () => {
const user = userEvent.setup();
render(<App />);
await user.click(screen.getByRole('link', { name: /products/i }));
await user.click(screen.getByRole('link', { name: /widget/i }));
user.(screen.(, { : }));
(screen.()).();
user.(screen.(, { : }));
(screen.()).();
});
React Patterns
function useToggle(initialOn = false) {
const [on, setOn] = useState(initialOn);
const toggle = () => setOn(prev => !prev);
const getTogglerProps = ({ onClick, ...props } = {}) => ({
'aria-pressed': on,
onClick: (...args) => {
onClick?.(...args);
toggle();
},
...props
});
return { on, toggle, getTogglerProps };
}
function App() {
const { on, getTogglerProps } = useToggle();
return (
<button
{...getTogglerProps({
onClick: () => console.log('clicked!'),
className: 'toggle-btn'
})}
>
{on ? 'ON' : 'OFF'}
</button>
);
}
function Toggle({ on: controlledOn, onChange, initialOn = false }) {
const [internalOn, setInternalOn] = useState(initialOn);
isControlled = controlledOn !== ;
on = isControlled ? controlledOn : internalOn;
() {
(!isControlled) {
( !prev);
}
onChange?.(!on);
}
;
}
Mental Model
Dodds approaches testing by asking:
- What does the user see? Query by visible elements
- What does the user do? Simulate real interactions
- What does the user expect? Assert on visible outcomes
- Does this test implementation? If yes, refactor the test
- Would this break on refactor? If yes, it's too coupled
Signature Dodds Moves
- Query by role first, test ID last
- userEvent over fireEvent
- MSW for network mocking
- Integration tests as the default
- Custom render with providers
- Test user behavior, not code structure