Minimal setup required to use react-testing-library with Jest.
All examples featured here run using these exact config files.
module.exports = { setupFilesAfterEnv: ['./rtl.setup.js']};
module.exports = {
setupFilesAfterEnv: ['./rtl.setup.js']
};
// See https://github.com/kentcdodds/react-testing-library#global-configimport 'jest-dom/extend-expect';import 'react-testing-library/cleanup-after-each';
// See https://github.com/kentcdodds/react-testing-library#global-config
import 'jest-dom/extend-expect';
import 'react-testing-library/cleanup-after-each';
The component renders variable text based on a string prop. We test that the component renders the value of the passed prop.
it('renders personalized greeting', async () => { // Render new instance in every test to prevent leaking state const { getByText } = render(<HelloMessage name="Satoshi" />); await waitForElement(() => getByText(/hello Satoshi/i));});
it('renders personalized greeting', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = render(<HelloMessage name="Satoshi" />);
await waitForElement(() => getByText(/hello Satoshi/i));
});
The component receives a callback prop and renders a button. We test that the callback prop is called when the button is clicked.
it('calls "onClick" prop on button click', () => { // Render new instance in every test to prevent leaking state const onClick = jest.fn(); const { getByText } = render(<Button onClick={onClick} />); fireEvent.click(getByText(/click me nao/i)); expect(onClick).toHaveBeenCalled();});
it('calls "onClick" prop on button click', () => {
// Render new instance in every test to prevent leaking state
const onClick = jest.fn();
const { getByText } = render(<Button onClick={onClick} />);
fireEvent.click(getByText(/click me nao/i));
expect(onClick).toHaveBeenCalled();
});
The component reads and updates a counter from its local state.
We test that the component renders the counter value. Then we click on the increment button, which updates the local state, and afterwards test that the component renders the incremented value.
We use
@react-mock/state
to mock the component state.
// Hoist helper functions (but not vars) to reuse between test casesconst renderComponent = ({ count }) => render( <StateMock state={{ count }}> <StatefulCounter /> </StateMock> );it('renders initial count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); await waitForElement(() => getByText(/clicked 5 times/i));});it('increments count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); fireEvent.click(getByText('+1')); await waitForElement(() => getByText(/clicked 6 times/i));});
// Hoist helper functions (but not vars) to reuse between test cases
const renderComponent = ({ count }) =>
render(
<StateMock state={{ count }}>
<StatefulCounter />
</StateMock>
);
it('renders initial count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
await waitForElement(() => getByText(/clicked 5 times/i));
});
it('increments count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
fireEvent.click(getByText('+1'));
await waitForElement(() => getByText(/clicked 6 times/i));
});
The component reads and updates a counter from the Redux store.
We test that the component renders the counter value. Then we click on the increment button, which updates the Redux state, and afterwards test that the component renders the incremented value.
// Hoist helper functions (but not vars) to reuse between test casesconst renderComponent = ({ count }) => render( <Provider store={createStore(counterReducer, { count })}> <ReduxCounter /> </Provider> );it('renders initial count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); await waitForElement(() => getByText(/clicked 5 times/i));});it('increments count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); fireEvent.click(getByText('+1')); await waitForElement(() => getByText(/clicked 6 times/i));});
// Hoist helper functions (but not vars) to reuse between test cases
const renderComponent = ({ count }) =>
render(
<Provider store={createStore(counterReducer, { count })}>
<ReduxCounter />
</Provider>
);
it('renders initial count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
await waitForElement(() => getByText(/clicked 5 times/i));
});
it('increments count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
fireEvent.click(getByText('+1'));
await waitForElement(() => getByText(/clicked 6 times/i));
});
The component is connected to React Router. It renders a variable text containing a URL parameter, as well as a Link
to another location.
First we make sure the component renders a param from the initial URL. Then we check that the URL param from a new location is rendered upon clicking on the Link element, which proves that the page has successfully routed.
Alternatively, we could just test the
to
prop of the Link element. That's also fine. But this test is closer to how a user thinks: Click on a link. Did the linked page open?This type of thinking makes tests more resilient against implementation changes, like upgrading the router library to a new API.
// Hoist helper functions (but not vars) to reuse between test casesconst renderComponent = ({ userId }) => render( <MemoryRouter initialEntries={[`/users/${userId}`]}> <Route path="/users/:userId"> <UserWithRouter /> </Route> </MemoryRouter> );it('renders initial user id', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ userId: 5 }); await waitForElement(() => getByText(/user #5/i));});it('renders next user id', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ userId: 5 }); fireEvent.click(getByText(/next user/i)); await waitForElement(() => getByText(/user #6/i));});
// Hoist helper functions (but not vars) to reuse between test cases
const renderComponent = ({ userId }) =>
render(
<MemoryRouter initialEntries={[`/users/${userId}`]}>
<Route path="/users/:userId">
<UserWithRouter />
</Route>
</MemoryRouter>
);
it('renders initial user id', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ userId: 5 });
await waitForElement(() => getByText(/user #5/i));
});
it('renders next user id', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ userId: 5 });
fireEvent.click(getByText(/next user/i));
await waitForElement(() => getByText(/user #6/i));
});
The component reads and updates a server counter using the Fetch API.
We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
These tests are async because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
We use
@react-mock/fetch
to mock the server requests.
// Hoist helper functions (but not vars) to reuse between test casesconst renderComponent = ({ count }) => render( <FetchMock mocks={[ { matcher: '/count', method: 'GET', response: { count } }, { matcher: '/count', method: 'POST', response: { count: count + 1 } } ]} > <ServerCounter /> </FetchMock> );it('renders initial count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); // It takes time for the counter to appear because // the GET request has a slight delay await waitForElement(() => getByText(/clicked 5 times/i));});it('increments count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); // It takes time for the button to appear because // the GET request has a slight delay await waitForElement(() => getByText('+1')); fireEvent.click(getByText('+1')); // The counter doesn't update immediately because // the POST request is asynchronous await waitForElement(() => getByText(/clicked 6 times/i));});
// Hoist helper functions (but not vars) to reuse between test cases
const renderComponent = ({ count }) =>
render(
<FetchMock
mocks={[
{ matcher: '/count', method: 'GET', response: { count } },
{ matcher: '/count', method: 'POST', response: { count: count + 1 } }
]}
>
<ServerCounter />
</FetchMock>
);
it('renders initial count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
// It takes time for the counter to appear because
// the GET request has a slight delay
await waitForElement(() => getByText(/clicked 5 times/i));
});
it('increments count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
// It takes time for the button to appear because
// the GET request has a slight delay
await waitForElement(() => getByText('+1'));
fireEvent.click(getByText('+1'));
// The counter doesn't update immediately because
// the POST request is asynchronous
await waitForElement(() => getByText(/clicked 6 times/i));
});
The component reads and updates a server counter using the XHR API.
We test that the component renders the counter value from the mocked API response. Then we click on the increment button, which makes a POST request to increment the counter, and afterwards test that the component renders the incremented value.
These tests are async because server requests don't resolve immediately. We wait for the button to appear before interacting with our component.
We use
@react-mock/xhr
to mock the server requests.
// Hoist helper functions (but not vars) to reuse between test casesconst getRes = count => async (req, res) => res.status(200).body({ count });const postRes = count => (req, res) => res.status(200).body({ count: count + 1 });const renderComponent = ({ count }) => render( <XhrMock mocks={[ { url: '/count', method: 'GET', response: getRes(count) }, { url: '/count', method: 'POST', response: postRes(count) } ]} > <ServerCounter /> </XhrMock> );it('renders initial count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); // It takes time for the counter to appear because // the GET request has a slight delay await waitForElement(() => getByText(/clicked 5 times/i));});it('increments count', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ count: 5 }); // It takes time for the button to appear because // the GET request has a slight delay await waitForElement(() => getByText('+1')); fireEvent.click(getByText('+1')); // The counter doesn't update immediately because // the POST request is asynchronous await waitForElement(() => getByText(/clicked 6 times/i));});
// Hoist helper functions (but not vars) to reuse between test cases
const getRes = count => async (req, res) => res.status(200).body({ count });
const postRes = count => (req, res) =>
res.status(200).body({ count: count + 1 });
const renderComponent = ({ count }) =>
render(
<XhrMock
mocks={[
{ url: '/count', method: 'GET', response: getRes(count) },
{ url: '/count', method: 'POST', response: postRes(count) }
]}
>
<ServerCounter />
</XhrMock>
);
it('renders initial count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
// It takes time for the counter to appear because
// the GET request has a slight delay
await waitForElement(() => getByText(/clicked 5 times/i));
});
it('increments count', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ count: 5 });
// It takes time for the button to appear because
// the GET request has a slight delay
await waitForElement(() => getByText('+1'));
fireEvent.click(getByText('+1'));
// The counter doesn't update immediately because
// the POST request is asynchronous
await waitForElement(() => getByText(/clicked 6 times/i));
});
The component reads and updates a value from LocalStorage.
We test that the component renders the mocked value from LocalStorage. Then we type a new name into an input, submit the form, and test that the submitted value has been updated in LocalStorage.
We use
@react-mock/localstorage
to mock the cached data.
// Hoist helper functions (but not vars) to reuse between test casesconst renderComponent = ({ name }) => render( <LocalStorageMock items={{ name }}> <PersistentForm /> </LocalStorageMock> );const submitForm = ({ getByText, getByLabelText }, { name }) => { fireEvent.change(getByLabelText('Name'), { target: { value: name } }); fireEvent.click(getByText(/change name/i));};it('renders cached name', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ name: 'Trent' }); await waitForElement(() => getByText(/welcome, Trent/i));});describe('on update', () => { it('renders updated name', async () => { // Render new instance in every test to prevent leaking state const utils = renderComponent({ name: 'Trent' }); submitForm(utils, { name: 'Trevor' }); await waitForElement(() => utils.getByText(/welcome, Trevor/i)); }); it('updates LocalStorage cache', () => { // Render new instance in every test to prevent leaking state const utils = renderComponent({ name: 'Trent' }); submitForm(utils, { name: 'Trevor' }); expect(localStorage.getItem('name')).toBe('Trevor'); });});
// Hoist helper functions (but not vars) to reuse between test cases
const renderComponent = ({ name }) =>
render(
<LocalStorageMock items={{ name }}>
<PersistentForm />
</LocalStorageMock>
);
const submitForm = ({ getByText, getByLabelText }, { name }) => {
fireEvent.change(getByLabelText('Name'), { target: { value: name } });
fireEvent.click(getByText(/change name/i));
};
it('renders cached name', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ name: 'Trent' });
await waitForElement(() => getByText(/welcome, Trent/i));
});
describe('on update', () => {
it('renders updated name', async () => {
// Render new instance in every test to prevent leaking state
const utils = renderComponent({ name: 'Trent' });
submitForm(utils, { name: 'Trevor' });
await waitForElement(() => utils.getByText(/welcome, Trevor/i));
});
it('updates LocalStorage cache', () => {
// Render new instance in every test to prevent leaking state
const utils = renderComponent({ name: 'Trent' });
submitForm(utils, { name: 'Trevor' });
expect(localStorage.getItem('name')).toBe('Trevor');
});
});
The component is styled using styled-components themes. This means the component requires ThemeProvider
context.
We're not testing style output here. The purpose of this test is merely to illustrate how to use ThemeProvider in tests.
// Hoist helper functions (but not vars) to reuse between test casesconst renderComponent = ({ theme, name }) => render( <ThemeProvider theme={theme}> <HelloMessageStyled name={name} /> </ThemeProvider> );it('renders greeting', async () => { // Render new instance in every test to prevent leaking state const { getByText } = renderComponent({ theme: themeLight, name: 'Maggie' }); await waitForElement(() => getByText(/hello Maggie/i));});
// Hoist helper functions (but not vars) to reuse between test cases
const renderComponent = ({ theme, name }) =>
render(
<ThemeProvider theme={theme}>
<HelloMessageStyled name={name} />
</ThemeProvider>
);
it('renders greeting', async () => {
// Render new instance in every test to prevent leaking state
const { getByText } = renderComponent({ theme: themeLight, name: 'Maggie' });
await waitForElement(() => getByText(/hello Maggie/i));
});
Presenting these examples took work. I hope they'll make your life easier!
There's a lot of wisdom that goes into writing clean tests. With every project I learn something new. I wanted to document my latest testing style and use it as a go-to resource for future projects.
The test examples are up to date and run in CircleCI. This searchable library is generated from README and test files available on GitHub.
The testing examples are opinionated. They aim to mimic user behavior and avoid testing abstract components. But you're free to disagree with my testing philosophy. The examples also feature modern libraries and agnostic testing techniques.
The component setup. Performing actions and assertions is already well documented by the tools that handle event simulation and/or expectations. The examples here focus on how to wire up various component types for testing.
Let's harness our collective knowledge to create a great resource for testing React components!
Best,
Ovidiu