DEV Community

Cover image for React Testing Simplified
Dan Jackson for Artlist

Posted on

React Testing Simplified

Javascript testing has a tendency to become unnecessarily complex - there's nearly always multiple solutions to the same problem.

This article aims to cover our chosen route at Artlist, the thinking behind these choices, and some simple code examples to get started with React Testing Library and Jest.

Why test a web frontend?

Confidence

When creating and maintaining increasingly complex web apps, we need the confidence that our app isn't going to fall over at a moment's notice. We need assurance that future changes will not break our components.

Depending on an app's user base; preventing bugs can save a company vast amounts of money in the long-run.

Design

Writing tests forces us to think about how a feature will be used. Each valid and invalid path through a feature can be described using tests. This also means that requirements can be easily proven.

Documentation

As a developer working on a new part of a shared codebase, how do you know what each file/component does?

If written correctly, tests provide a specification of a feature. This is why tests are often saved with a .spec extension.

test('clicking save button submits the form' , () => {});

test('clicking close button closes modal' , () => {});
Enter fullscreen mode Exit fullscreen mode

Jest

As Jest is the default test runner for React Testing Library, we will use this for code examples.

Jest allows us to:

  1. describe our test suite
  2. Create each test case
  3. Define what we expect each test case to do
describe('complex calculations', () => {
  test('1 + 1 = 2', () => {
    expect(1 + 1).toEqual(2);
  });
});
Enter fullscreen mode Exit fullscreen mode

Mocking

Jest also provides utilities to mock functions and modules. Mocks replace functionality with a function that Jest can use to check:

  • If a mock function was called
  • How many times it was called
  • Which parameters it was called with
const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();
Enter fullscreen mode Exit fullscreen mode

Mocks can also be useful in components that contain external functionality that we don't want to use in our tests. If you do need to retain functionality use jest.spyOn - this can call the original function but return a mock function.


React Testing Library

React Testing library is a set of helpers that can be used in conjunction with a test runner such as Jest to render React components and make assertions on the resulting DOM elements.

The main functions are:

  • render: Renders a react component
  • getBy*/findBy*/queryBy*: A collection of matcher functions to find elements in the DOM
  • userEvent: Emulate user events such as click or type

React Testing library is best suited to creating Unit and Integration tests; isolated sections of our app that can be tested individually without the influence of components higher up the tree.


Unit Test

Unit tests are best performed on pure components - components that will always give the same output given the same props. These kind of tests are usually performed on basic visual components that are used to form larger structures and sections in our app.

const Button = ({text}) => (
  <button>{children}</button>
);
Enter fullscreen mode Exit fullscreen mode
import { render, screen } from '@testing-library/react';

test('button text is displayed correctly',  () => {
  render(<Button>Click Me</Button>);
  expect(screen.getByText("Click Me")).toBeInTheDocument();
}); 
Enter fullscreen mode Exit fullscreen mode

Integration Test

An integration test proves that multiple components work correctly together. In the context of React, this usually consists of rendering a compound component, and interacting with it in some way.

import { render, screen, userEvent } from '@testing-library/react';

import handleSubmit from './handleSubmit';

jest.mock('./handle-submit');

test('can submit form with first and last name',  () => {
  render(<Form/>);

  userEvent.type(screen.getByLabelText('firstname'), 'Dan');
  userEvent.type(screen.getByLabelText('lastname'), 'Jackson');
  userEvent.click(screen.getByText('submit'));

  expect(handleSubmit).toHaveBeenCalledWith('Dan Jackson');
}); 
Enter fullscreen mode Exit fullscreen mode

In this example, handleSubmit is an imaginary function that would be used inside our Form component. By calling jest.mock('./handle-submit'), we are replacing the actual implementation with a mock function. Without a mock, we cannot test in this way.

We think it's important to create components in a composable way; making sure that responsibility is split into child components and not aggregated in one place. You should not need to render an entire page just to test one form control.


End-to-End?

E2E testing involves testing that entire pages function correctly when interacted with. This kind of testing is outside the realms of React Testing Library, and here at Artlist we have Automation Engineers working on adding the E2E framework Playwright to our infrastructure.

Whilst E2E testing inherently provides the most overall coverage (and therefore code confidence), it is still important to test your application at various different levels. Kent C. Dodds has an Excellent Article explaining how utilising a variety of test types can be beneficial.

Crude diagram showing test types


Dynamic Content

Components using animations or transitions can be difficult to test due to the the varying DOM output at any given time. For example, you may have an animated notification that appears after a delay, or an exit animation that doesn't remove an element from the DOM until that animation has finished.

Take Artlist's download button for example:
artlist download animation

React Testing Library has various async utilities for this exact purpose, such as waitFor.

import { render, screen, userEvent, waitFor } from '@testing-library/react';

test('clicking download shows success notification',  () => {
  render(<Form/>);

  userEvent.click(screen.getByLabelText('download song'));
  await waitFor(() => {
    expect(getByText('Download Successful')).toBeInTheDocument()
  });
}); 
Enter fullscreen mode Exit fullscreen mode

waitFor tells jest to wait until the assertions inside the callback are fulfilled, or the test timeout has been reached.


Why not use Enzyme?

Enzyme is another popular React testing utility - but it operates in a fundamentally different way. With Enzyme, we are given the ability to test the internals of our components, such as props, state, and lifecycle methods.

React Testing Library on the other hand, gives no access to implementation details; instead providing ways to render components and interact with them. This means a steeper learning-curve, but tests that are closer to real-world user interactions.


Conclusion

Here at Artlist we are always on the lookout for ways to make our developer's lives easier; and utilising a tried-and-true React testing framework to provide code confidence is one of the ways we are working towards that goal.

Next Steps

Check out the official React Testing Library docs for in depth explanations, a full API reference, and more advanced topics such as configuration options.

Happy testing!

Top comments (3)

Collapse
 
victordep profile image
vuongvgc

Great overview about test in react

Collapse
 
raphaelartlist profile image
Raphael Ben Hamo

Champ!

Collapse
 
edwardprogrammer profile image
Edwardprogrammer

Great post