Testing with react-testing-library and Jest

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces
Published in
7 min readAug 4, 2020

--

Errors and bugs are a fact of life. There’s no way to avoid them. The only thing we can do is to minimize the chances of them by:

  1. Identifying errors and weeding them out using automated testings (the subject of this blog post).
  2. Reusing components that have “proven” themselves to be error-free both in real projects, as well as in automated testings. This can be done using Bit’s tool and platform (Github). Bit makes it quick and easy to share, reuse, and manage reusable components. It even runs unit tests before sharing components to Bit’s component hub.
    I won't discuss this any further, in this blog post, but you can read more about it here:

One aspect of testing is UI testing.

UI testing is a technique used to test the graphical user interface. It involves checking the screens, the different controls, etc., to ensure they work as intended under different conditions.

In this post, we’ll look at two libraries that enable us to carry out UI testing in our React application:

React-Testing-Library

React-Testing-Library is a DOM-testing library developed by Kent. C Dodds. It is a replacement for Enzyme and provides light utility functions for use with react-dom.

It is targeted more at UI testing of React applications. This means that it does not only test the instances of React components but can also test the behaviors of the elements rendered by the React components. So it is an awesome library for React UI testing.

React Testing Library forces us to follow the principle:

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

Snapshot testing

Snapshot testing is a feature in react-testing-feature in which we can get the snapshot of a React component and test them. This is important because we will add or update the component and might want to compare the changes.

Let’s see how we can perform UI testing on React components using react-testing-library

Selecting and Testing DOM Elements

React-Testing-Library renders React components like a browser and we can select the elements just as we can using the DOM APIs or from the browser Inspector tab.

One way of selecting an element is via the getByText function.

getByText

This function grabs a text from the screen. It returns the text node of the text and with it, we can use it for assertations or for user interaction.

Example:

We have an App component:

import React from 'react';function App() {
return (
<div>
React Example
</div>
);
}
export default App;

Let’s create a test file to assert that the App component renders a “React Example” text on the screen. To do we create an App.test.js file with the code:

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.getByText('React Example');
});
});

What did we do here? We imported the React module, and also imported the render and screen from ‘@testing-library/react’. These functions are the basic functions for testing React components. render renders the React component in a virtual browser and screen is used to access the rendered DOM.

Next, we imported our App component, created a test suite and a test. There we rendered our App component using the render function and used the getByText method in the screen to get the text “React Example” from the component.

The screen.getByText returns matchers that we can use to make different assertations.

  • toBeNull
  • toBeInTheDocument
  • toBeDisabled
  • toBeEnabled
  • toBeEmpty
  • toBeEmptyDOMElement
  • toBeInvalid
  • toBeRequired
  • toBeValid
  • toBeVisible
  • toContainElement
  • toContainHTML
  • toHaveAttribute
  • toHaveClass
  • toHaveFocus
  • toHaveFormValues
  • toHaveStyle
  • toHaveTextContent
  • toHaveValue
  • toHaveDisplayValue
  • toBeChecked
  • toBePartiallyChecked
  • toHaveDescription

Let’s demonstrate a few:

toBeNull: asserts that the text node is null, i.e it does not exist.

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
// fails
expect(screen.getByText('React Example')).toBeNull();
});
});

The above test will fail because there is a text node with the value “React Example” in the DOM rendered by our App component.

toBeInTheDocument

This checks if the text node is present in the DOM rendered by a component.

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
// succeeds
expect(screen.getByText('React Example')).toBeInTheDocument();
});
});

This succeeds because a “React Example” is in the App DOM.

getByTestId

The render function also returns functions that we can use to make further assertations on the rendered component. One of them is getByTestId.

this function returns the DOM instance of the element with the specified dataset attribute “data-testid”.

Example:

import React from 'react';function App() {
return (
<div data-testid="mainDiv">
<h3 data-testid="mainHeader">
React Example
</h3>
<h3 data-testid="sideHeader">
React Example 2
</h3>
</div>
);
}
export default App;

OK, we have elements with data-testid attribute. With these, we can use the getByTestId to select the elements by passing the value of their data-testid to the function.

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
const { getTestById } = render(<App />);
// succeeds
expect(getTestById("mainHeader")).toHaveTextContent("React Example")
// succeeds
expect(getTestById("sideHeader")).toHaveTextContent("React Example 2")
});
});

The toHaveTextContent matcher function checks the element has a child text node with the passed in value. So “mainHeader” and “sideHeader” succeeds because the elements have texts “React Example” and “React Example 2”.

Event Testing

We can test for DOM events in our React components using react-testing-library.

DOM events like:

  • click
  • mouse events
  • key events
  • touch events
  • etc

Let’s refactor our App component so we can test for click events.

import React, { useState } from 'react';function App() {
const { state, setState } = useState(0)
return (
<div>
<div>
State: <span data-testid="display">{state}</span>
</div>
<div>
<button data-testid="button1" onClick={setState(1)}>1</button>
<button data-testid="button2" onClick={setState(2)}>2</button>
<button data-testid="button3" onClick={setState(3)}>3</button>
</div>
</div>
);
}
export default App;

Our App component has a state state with initial value 0. We have three buttons 1, 2,3 that when clicked will change the state to 1, 2, or 3 respectively.

Let’s see the test:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
const { getByTestId } = render(<App />); expect(getByTestId('display')).toHaveTextContent('0') fireEvent.click(getByTestId('button1')) expect(getByTestId('display')).toHaveTextContent('1') fireEvent.click(getByTestId('button2')) expect(getByTestId('display')).toHaveTextContent('2') fireEvent.click(getByTestId('button3')) expect(getByTestId('display')).toHaveTextContent('3')
});
});

First, to use events we imported the fireEvent from the react-testing-library. As we are testing for click events we will call the click method from the fireEvent object.

Initially, the “display” would have “0” rendered. So the test succeeds.

Next, we click the “button1”. This will change the state to 1 and “display” will have “1” as its text content so the test succeeds.

Next, we click “button2”, this will set the state to 2, so the test on “display” text content to be true succeeds.

Last we click on “button3” and the state is set to 3. So the test on “display” with text “3” passes. So finally all of our tests will pass.

It is not actually clicking but firing the click event on the events is what happens. Just like we can do this programmatically.

button1.dispatchEvent(new Event("click"))

Async Testing

Asynchronous actions can be tested in React components using react-testing-library.

In this example, we use the setTimeout function to show how we can use react-testing-library to test async actions.

Asynchronous actions break the flow of execution in JavaScript, so testing libraries have a way to wait for them to complete before running the assertions on the resolved values.

Here is an example:

import React, { useState } from 'react';function App() {
const { state, setState } = useState(0)
const setStateAsync = (value) => {
setTimeout(() => {
setState(value)
}, 1000)
}
return (
<div>
<div>
State: <span data-testid="display">{state}</span>
</div>
<div>
<button data-testid="button1" onClick={setStateAsync(1)}>Button1</button>
<button data-testid="button2" onClick={setStateAsync(2)}>Button2</button>
<button data-testid="button3" onClick={setStateAsync(3)}>Button3</button>
</div>
</div>
);
}
export default App;

Still using our previous example, but this time it is setting the state asynchronously using the setTimeout.

When any of the buttons is clicked the setStateAsync will set a timeout, will the rest of the code completes. Now when the passed time of 1s elapses, the state is set and the view is updated. Because it is async the UI will not freeze during the whole time, it will run finish, and the event loop system will kick in and execute the setTimeout function handler.

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
const { getByTestId } = render(<App />); expect(getByTestId('display')).toHaveTextContent('0') await fireEvent.click(getByTestId('button1')) expect(getByTestId('display')).toHaveTextContent('1') await fireEvent.click(getByTestId('button2')) expect(getByTestId('display')).toHaveTextContent('2') await fireEvent.click(getByTestId('button3')) expect(getByTestId('display')).toHaveTextContent('3')
});
});

We added the await keyword to the fireEvent.click to wait for the setTimeout to timeout and set the state before it can continue.

The first assertion checks the “display” would have an initial text content of “0”. Then, will fire the “click” event on “button1” and waits for it to return. Upon return, the “display” will have been set, then it asserts that the “display” has a text content of “2”, and so on till the last.

See? Async testing in React components using react-testing-library is very simple.

Conclusion

Testing in React using react-testing-library is just a smooth ride. Enzyme comes closer though but react-testing-library makes testing human-like and can test your user behaviors because it’s humans that will use your products.

Please, feel free to ask if you have any questions or comments in the comment section.

Thanks !!!

Learn More

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕