How to Test User Interactions Using the React Testing Library

As a frontend developer working with React, testing is a crucial skill to ensure your application works as expected and provides a great user experience. While unit tests help verify the behavior of individual functions and components in isolation, it‘s equally important to write tests that simulate real user interactions with your app.

This is where the React Testing Library comes in. It provides a set of tools and best practices to write maintainable tests for React components. Unlike previous solutions like Enzyme which encouraged testing implementation details, React Testing Library takes a more user-centric approach and tests the actual DOM output.

In this in-depth guide, we‘ll learn how to effectively test user interactions in a React app using the React Testing Library. We‘ll cover everything from setting up a project to writing robust interaction tests and avoiding common pitfalls. Even if you‘re new to React testing, by the end of this article you‘ll have the confidence to write tests like a pro!

Setting up React Testing Library

The easiest way to get started with React Testing Library is by creating a new app using Create React App (CRA). CRA already comes preconfigured with Jest as the test runner and React Testing Library installed.

If you have an existing project, you can install Testing Library and its companion user-event module as dev dependencies:

npm install --save-dev @testing-library/react @testing-library/user-event

We also recommend installing the jest-dom package which provides a bunch of useful custom matchers:

npm install --save-dev @testing-library/jest-dom

To use these matchers, import them in your test files or add this line to the src/setupTests.js file:

import ‘@testing-library/jest-dom‘;

Querying DOM Elements

The first step to testing user interactions is querying the DOM elements that users will interact with, such as buttons, links, form inputs etc. Testing Library provides several querying methods prefixed with getBy, queryBy and findBy.

The recommended approach is to query elements by their role or accessible name. This ensures your tests work even if the underlying DOM structure or CSS classes change. Here are some examples:

// Query a button by its text
screen.getByText(‘Submit‘)

// Query a link by its accessible name
screen.getByRole(‘link‘, { name: /sign up/i })

// Query an input by its label text
screen.getByLabelText(‘Email‘)

If there‘s no appropriate role or accessible name, you can also query elements by their placeholder text, alt text, test ID, or even plain text:

// Query an input by placeholder
screen.getByPlaceholderText(‘Enter your name‘)

// Query an image by alt text 
screen.getByAltText(‘Profile picture‘)

// Query a element by test ID
screen.getByTestId(‘submit-button‘)

The getBy* methods throw an error if no matching element is found. If you expect an element to not be present, use the queryBy* methods instead which return null.

For elements that may not appear immediately (e.g. fetched asynchronously), use the findBy* methods which return a Promise that resolves when the element appears in the DOM.

Simulating User Events

Now that we know how to query DOM elements, let‘s see how to simulate different user interactions using the user-event library.

Clicking buttons and links

To simulate a click event, first query the button or link element and then call user.click():

test(‘clicking the button increments the count‘, () => {
  render(<Counter />);

  const button = screen.getByText(‘Increment‘);
  const countText = screen.getByText(/count: 0/i);

  user.click(button);

  expect(countText).toHaveTextContent(‘Count: 1‘);
});

Typing into form fields

To simulate typing into a text input or textarea, use the user.type() method:

test(‘typing updates the input value‘, () => {
  render(<LoginForm />);

  const emailInput = screen.getByLabelText(‘Email‘);

  user.type(emailInput, ‘[email protected]‘);

  expect(emailInput).toHaveValue(‘[email protected]‘);  
});

To clear the input first, pass an object with the initialSelectionStart and initialSelectionEnd options set to 0:

user.type(emailInput, ‘[email protected]‘, { 
  initialSelectionStart: 0,
  initialSelectionEnd: 0,
});

Selecting dropdown options

To select an option in a dropdown (<select> element), use the user.selectOptions() method:

test(‘selecting a country updates the value‘, () => {
  render(<AddressForm />);

  const countrySelect = screen.getByLabelText(‘Country‘);

  user.selectOptions(countrySelect, ‘United States‘);

  expect(countrySelect).toHaveValue(‘US‘);
});  

You can also pass an array to user.selectOptions() to select multiple options in a <select multiple> dropdown.

Hovering over elements

Some interactions like tooltips or dropdown menus are triggered on hover. To simulate hovering over an element, use user.hover():

test(‘hovering over an item displays a tooltip‘, () => {
  render(<ListItem />);  

  const item = screen.getByText(‘Item 1‘);

  user.hover(item);

  const tooltip = screen.getByRole(‘tooltip‘);
  expect(tooltip).toBeInTheDocument();

  user.unhover(item);
  expect(tooltip).not.toBeInTheDocument();
});

Uploading files

To test file uploading, create a File object with the desired file data and pass it to user.upload():

test(‘uploading a file updates the file input‘, () => {
  render(<ImageUploader />);

  const file = new File([‘(⌐□_□)‘], ‘chucknorris.png‘, { type: ‘image/png‘ });
  const fileInput = screen.getByLabelText(/choose an image/i);

  user.upload(fileInput, file);

  expect(fileInput.files[0]).toBe(file);
  expect(fileInput.files[0].name).toBe(‘chucknorris.png‘);
});  

Filling and submitting forms

To test a complete form submission flow, you can combine the individual field interactions and then submit the form. Let‘s see an example:

test(‘submitting the signup form with valid data‘, () => {
  render(<SignupForm />);

  const nameInput = screen.getByLabelText(‘Name‘);
  const emailInput = screen.getByLabelText(‘Email‘);  
  const passwordInput = screen.getByLabelText(‘Password‘);
  const submitButton = screen.getByText(‘Sign up‘);

  user.type(nameInput, ‘John Doe‘);
  user.type(emailInput, ‘[email protected]‘);
  user.type(passwordInput, ‘super-secret‘);
  user.click(submitButton);

  expect(screen.getByText(‘Welcome John!‘)).toBeInTheDocument();
});

We filled in the name, email and password fields, submitted the form, and asserted that the welcome message is shown.

If your form can be submitted by pressing Enter, you can simulate that by passing {key: ‘Enter‘} to user.type():

user.type(passwordInput, ‘super-secret‘, {key: ‘Enter‘});

Writing Effective Assertions

After simulating user interactions, the next step is to verify the expected outcome by making assertions. Testing Library extends the Jest assertion library with custom DOM-based matchers via @testing-library/jest-dom.

Some commonly used assertions are:

  • .toBeInTheDocument() – assert an element is in the DOM
  • .toHaveTextContent() – assert an element has the expected text content
  • .toHaveValue() – assert a form field has the expected value
  • .toBeVisible() – assert an element is visible to the user
  • .toBeDisabled() – assert a button or form field is disabled
  • .toHaveClass() – assert an element has certain class name(s)
  • .toHaveAttribute() – assert an element has the expected attribute value
  • .toHaveStyle() – assert an element has certain inline style(s)

For example, here‘s how we can assert a button is disabled after clicking it:

test(‘clicking the button disables it‘, () => {
  render(<SubmitButton />);

  const button = screen.getByText(‘Submit‘);

  expect(button).toBeEnabled();

  user.click(button);

  expect(button).toBeDisabled();
});

We recommend avoiding assertions tied to the implementation (e.g. CSS classes or specific DOM structure) and instead focus on testing the user-visible behavior.

Tips for Writing Better Tests

Here are some tips to keep in mind while writing tests with React Testing Library:

  1. Avoid testing implementation details: Tests that rely on internal component state or specific DOM structure tend to break easily when the implementation changes. Instead, test the external API and observable behavior.

  2. Write accessible queries: Prefer querying elements by their role, accessible name or label. This ensures your app is accessible and tests are resilient to markup changes.

  3. Keep tests focused: Each test should verify one specific behavior or user flow. Avoid testing multiple things in a single test. This makes tests easier to understand and maintain.

  4. Isolate tests from each other: Avoid shared state between tests which can lead to flakiness. If necessary, clean up any side effects (e.g. clear a global cache or reset mocked API responses) before each test using beforeEach().

  5. Handle asynchronous behavior: For elements that are rendered asynchronously (e.g. after data fetching), use findBy queries or waitFor with getBy queries to wait for the element to appear. Avoid arbitrary timeouts which can lead to flaky tests.

Testing with TypeScript

Testing Library has great support for TypeScript. When creating a new project with CRA and the --template typescript flag, the necessary type definitions are automatically installed.

One common pattern is to create a custom render function that wraps the render method from Testing Library. This allows defining the type of the props that your component expects:

import { render, RenderOptions } from ‘@testing-library/react‘;

interface ComponentProps {
  label: string;
  onClick: () => void;  
}

function renderComponent(props: Partial<ComponentProps>, options?: RenderOptions) {
  const defaultProps: ComponentProps = {
    label: ‘Default label‘,
    onClick: jest.fn(),
  };

  return render(<MyComponent {...defaultProps} {...props} />, options);
}

Now you can use the renderComponent function in your tests to get type inference for the props. We also defined default values for the required props to avoid duplication in each test.

Advanced Testing Scenarios

Apart from the basic user interactions we covered so far, you might also need to test some advanced scenarios:

  • Mocking API requests: If your component makes API calls, you‘ll need to mock the requests in tests to avoid flakiness and improve test speed. You can use a library like msw to intercept and mock API requests.

  • Testing custom hooks: You can test custom React hooks by creating a test component that uses the hook and making assertions on the component‘s behavior.

  • Visual regression testing: For components that heavily rely on visual styles (e.g. a complex chart or interactive map), you can use visual regression testing tools like jest-image-snapshot or Chromatic to catch unintended visual changes.

Explaining these advanced topics in detail is beyond the scope of this article, but you can find links to learn more in the resources section below.

Learning More

Congratulations on making it this far! You now have a solid foundation for testing user interactions in React apps with confidence. But there‘s always more to learn. Here are some resources to level up your React testing skills:

Also check out the larger Testing Library family which includes adapters for Vue, Angular, Svelte, and other frameworks.

Conclusion

In this guide, we learned how to test user interactions in React applications using the React Testing Library. We covered the basics of querying elements, simulating events, and writing assertions, along with some best practices and advanced techniques to take your tests to the next level.

Remember, the key to writing effective tests is to think from a user‘s perspective. Focus on testing the app‘s observable behavior rather than implementation details. Keep your tests isolated, focused and maintainable. And always strive to make your app more accessible – your users and your tests will thank you for it!

What are your favorite techniques for testing React components? Let me know in the comments below or reach out on Twitter at @myusername. Happy testing!

Similar Posts