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' , () => {});
Jest
As Jest is the default test runner for React Testing Library, we will use this for code examples.
Jest allows us to:
-
describe
our test suite - Create each
test
case - Define what we
expect
each test case to do
describe('complex calculations', () => {
test('1 + 1 = 2', () => {
expect(1 + 1).toEqual(2);
});
});
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();
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>
);
import { render, screen } from '@testing-library/react';
test('button text is displayed correctly', () => {
render(<Button>Click Me</Button>);
expect(screen.getByText("Click Me")).toBeInTheDocument();
});
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');
});
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.
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:
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()
});
});
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)
Great overview about test in react
Champ!
Great post