React Testing Library

Enzyme (before React-testing-library)

  • Shallow vs Deep rendered components
  • Direct testing of prop values on components
  • True isolated "unit" testing

"The Problem: You want to write maintainable tests for your React components. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down."

The more your tests resemble the way your software is used, the more confidence they can give you.

Forces you to test the DOM in a way the user would interact with your code

  • Labels and text
  • Roles
  • Find elements by a data-testid as an "escape hatch" for elements where the text content and label do not make sense or is not practical

Pros and Cons

React Testing Library

Enzyme

Pros

Cons

Testing the way users interact with the site

Harder to test specific use cases

easy to quickly test use cases

Duplicated coverage

(Tons of mocks)

Integration-focused/focus on real scenarios

testing implementation details

constantly having to "fix" tests

tons of utilities / very powerful

What to test?

Think in User Flows and Boundaries

Write fewer, longer, more comprehensive tests

Test boundary - The API loaded, user makes changes, clicks submit, and then saw a success message

[GET API] -> [UI screen] -> [Updates] -> [Button Click] -> [Success Message] -> [POST API]

"Business Logic"

Heavy business logic should be tested at the unit level

Business Logic -> If / elses

Mapping or other complicated logic (if elses) should have a unit test (functions that aren't react components)

Put complicated logic in hooks - and test that (it's much easier)

100% coverage?

Best Practices

Selectors (priority order)

  1. Queries Accessible to Everyone
    • getByRole
    • getByLabelText
    • getByPlaceholderText
    • getByText
    • getByDisplayValue
  2. Semantic Queries
    • getByAltText
    • getByTitle
  3. Test IDs
    • getByTestId

GetBy vs QueryBy vs FindBy

explicit assertion

import React from 'react';
import { render, screen } from '@testing-library/react';
 
import App from './App';
 
describe('App', () => {
  test('renders App component', () => {
    render(<App />);
 
    // implicit assertion
    // because getByText would throw error
    // if element wouldn't be there
    screen.getByText('Search:');
 
    // explicit assertion
    // recommended
    expect(screen.getByText('Search:')).toBeInTheDocument();
  });
});
import React from 'react';
import { render, screen } from '@testing-library/react';
 
import App from './App';
 
describe('App', () => {
  test('renders App component', () => {
    const { container } = render(<App />);
 
    /** don't do this **/
    const myElement = container.querySelectorAll('.some-class');
    
  });
});

🚨 If you find yourself using container to query for rendered elements then you should reconsider! The other queries are designed to be more resilient to changes that will be made to the component you're testing. Avoid using container to query for elements!

events

Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.

callbacks

describe('Search', () => {
  test('calls the onChange callback handler', () => {
    const onChange = jest.fn();
 
    render(
      <Search value="" onChange={onChange}>
        Search:
      </Search>
    );
 
    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });
 
    expect(onChange).toHaveBeenCalledTimes(1);
  });
});

How do I fix "an update was not wrapped in act(...)" warnings?

This warning is usually caused by an async operation causing an update after the test has already finished. There are 2 approaches to resolve it:

  1. Wait for the result of the operation in your test by using one of the async utilities like waitFor or a find* query. For example: const userAddress = await findByLabel(/address/i).
  2. Mocking out the asynchronous operation so that it doesn't trigger state updates.

Generally speaking, approach 1 is preferred since it better matches the expectations of a user interacting with your app.

Debugging

Debugging

screen.debug()

import { screen } from '@testing-library/dom'

document.body.innerHTML = `
  <button>test</button>
  <span>multi-test</span>
  <div>multi-test</div>
`

// debug document
screen.debug()
// debug single element
screen.debug(screen.getByText('test'))
// debug multiple elements
screen.debug(screen.getAllByText('multi-test'))

screen.logTestingPlaygroundURL()

import { screen } from '@testing-library/dom'

document.body.innerHTML = `
  <button>test</button>
  <span>multi-test</span>
  <div>multi-test</div>
`

// log entire document to testing-playground
screen.logTestingPlaygroundURL()
// log a single element
screen.logTestingPlaygroundURL(screen.getByText('test'))

Resources

React Testing Library

By Alex Voerman

React Testing Library

  • 246