React Testing Simplified
Tags: javascript, react, testing
Published: 2022-10-03 12:47:46

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. 


```javascript
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:

1. `describe` our test suite
2. Create each `test` case
3. Define what we `expect` each test case to do

```javascript
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

```javascript
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](https://testing-library.com/react) is a set of helpers that can be used in conjunction with a test runner such as [Jest](https://jestjs.io/) 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.

```javascript
const Button = ({text}) => (
  <button>{children}</button>
);
```

```javascript
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.

```javascript
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](https://playwright.dev/) 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](https://kentcdodds.com/blog/static-vs-unit-vs-integration-vs-e2e-tests) explaining how utilising a variety of test types can be beneficial.

![Crude diagram showing test types](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcaqbcrs0nboyk6pbfhg.png) 

---


## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iscjiov2p4bwl91cvb1d.gif)
 

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

```javascript
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](https://enzymejs.github.io/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](https://testing-library.com/docs/) for in depth explanations, a full API reference, and more advanced topics such as configuration options.

Happy testing!