React Testing Examples

react-testing-library
GitHubAbout
All examples

Minimal setup required to use react-testing-library with Jest.

All examples featured here run using these exact config files.

jest.config.js
module.exports = {
setupFilesAfterEnv: ['./rtl.setup.js']
};
module.exports = {
  setupFilesAfterEnv: ['./rtl.setup.js']
};
rtl.setup.js
// See https://github.com/kentcdodds/react-testing-library#global-config
import '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.

test.js
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.

test.js
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.

test.js
// 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));
});
// 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.

test.js
// 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));
});
// 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.

test.js
// 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));
});
// 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.

test.js
// 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));
});
// 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.

test.js
// 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));
});
// 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.

test.js
// 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');
});
});
// 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.

test.js
// 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));
});
// 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));
});

Dear friend,

Presenting these examples took work. I hope they'll make your life easier!

Why put this together?

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.

How does it work?

The test examples are up to date and run in CircleCI. This searchable library is generated from README and test files available on GitHub.

Is this useful to you?

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.

What's the focus?

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.

Want to contribute?

Let's harness our collective knowledge to create a great resource for testing React components!

Best,
Ovidiu