Intro
In this tutorial I will be giving an introduction to testing in React. Testing and Test Driven Development is becoming more and more popular in the React community, and for good reasons:
- Time: Having a suite of tests you can run after any changes to your app will actually save you time in the long term
- Functionality: Having tests can ensure your app works exactly how you intend it to
- Intentionality: Writing tests before you write your app forces you to put more forethought into your code
To learn about testing, we will be building a simple To Do app, and writing some tests using Jest and React Testing Library to verify its functionality. We will cover:
- Creating a basic app
- Setting up your directory structure
- describe, it, expect and other Jest concepts
- screen, render and other React Testing Library concepts
- Running your tests
You should have a basic level of familiarity with React to follow this tutorial.
Setup
The first thing we’ll do is create a new React Application using Create-React-App (CRA). The advantage of this is that CRA sets us up with the libraries and scripts we need from the get go!
npx create-react-app learning-testing
The next step is deleting all the boilerplate that CRA comes with. When you’re finished, your /src folder should only have an App.js and an Index.js file. Your App.js file should look like this:
import React from 'react'
function App() {
return (
<div>
</div>
)
}
export default App
Your index.js file should look like this:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
To Do App
The next step is to build a simple to-do application. I won’t spend too long explaining the to-do app, as I am assuming you’re familiar with React already. Users should be able to enter some text and create a new to do item by clicking a button. Some simple state hooks monitor the input and build an array of to-do items. My code looked like this:
import { useState } from 'react';
function App() {
const [input, setInput] = useState('');
const [todoItems, setTodoItems] = useState([]);
function updateInput(e) {
setInput(e.target.value);
}
function addItem(e) {
e.preventDefault();
setTodoItems([...todoItems, input]);
}
return (
<div className="App">
<h1>Todo App</h1>
<form>
<p>Add a new ToDo</p>
<input data-testid="todo-input" onChange={updateInput}></input>
<button onClick={addItem}>Add</button>
</form>
<div>
{todoItems.map((item) => (
<p key={item}>{item}</p>
))}
</div>
</div>
);
}
export default App;
If you look closely, you might notice something you haven’t seen before. Our <input> tag has a strange property:
<input data-testid="todo-input" onChange={updateInput}></input>
This “data-testid” property is something we will use later on to access the input in our tests.
Testing
With our to-do app created, it is time to start writing some tests! The first thing we will need to do is set up our directory structure. As a child of the /src folder, create a __test__ folder. Any files in here will automatically get executed when we run our test script. Next, create a file in your test folder called App.test.js. A common pattern for structuring your test directory is to have one test file for each component we are creating. If you had a component called Form.js, you would have a file called Form.test.js in your __test__ directory. Because we only have the <App> component, we will only create one test file! Your setup should look like this:
/src/__test__/App.test.js
Here’s what we’re going to test:
- Does the <App> component successfully render?
- Can you create a new to-do item by interacting with the App?
I’m going to show you all the tests I wrote, and I’ll explain each piece one at a time. My tests looked like this:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import App from '../App';
describe('Todo App', () => {
it('should render the App component', () => {
render(<App />);
expect(screen.getByRole('heading')).toHaveTextContent('Todo App');
});
it('should create a new todo item', () => {
const { getByText } = render(<App />);
userEvent.type(screen.getByTestId('todo-input'), 'learn testing!');
userEvent.click(screen.getByRole('button'));
expect(getByText(/learn testing!/i)).toBeInTheDocument();
});
});
The first thing I want to explain are the keywords describe, it and expect. These are operators used by the Jest library. describe is used to house a suite of tests, and you usually use one describe block per .test file. The first argument is the title of the test suite, and the second is a callback within which you run your tests.
it is used to define an individual test. You can also use the keyword test, but I prefer to use it because I find it more semantic. The tests end up reading like plain english, which I like.
Finally, we have expect. Expect is used to define boolean operations, and the result of your expect block will determine if your test passes or fails. On the right side of the expect block you can add an operator such as .toBe() or .equals(). These are called matchers, and they are used to evaluate what you have inside your expect block.
For a full reference of all the matchers you can use, see below:
There are too many matchers and different ways to use them for me to give a full breakdown here, but I encourage you to give the documentation a look, and play around with using them to write your own tests!
Test 1: Render
Next I’ll explain the complete logic of this test:
render(<App />);
expect(screen.getByRole('heading')).toHaveTextContent('Todo App');
We start by rendering our App component. This lets us access DOM elements through the screen object. We use .getByRole to query the DOM for our <h1> we defined earlier:
<h1>Todo App</h1>
If everything goes according to plan, our test will be successful, because our <h1> has the text content we’re expecting it to.
Test 2: User Events
const { getByText } = render(<App />);
userEvent.type(screen.getByTestId('todo-input'), 'learn testing!');
userEvent.click(screen.getByRole('button'));
expect(getByText(/learn testing!/i)).toBeInTheDocument();
In this test we’re making use of a really cool part of React Testing Library called userEvent. userEvent is used to simulate user interactions with our UI. We’re also coming back around to the “data-testid” property we discussed earlier. We use .getByTestId to access our input through that property, and use userEvent to simulate typing in a new to-do item and clicking the submit button. Finally, we use the match .toBeInTheDocument to query the entire document for our keywords. If our app successfully adds a new to-do item, our test will pass!
Running Our Tests
The last step in the process is to run our tests. Another great reason to use Create-React-App is that it will set up your test scripts for you. Open your command line inside of your project repository and type:
npm test
If everything goes well, you should see an output like this:
PASS src/__test__/App.test.js
Todo App
✓ should render the App component (52 ms)
✓ Should create a new todo item (50 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.836 s, estimated 1 s
Ran all test suites related to changed files.
You have now successfully tested your React Application!
💬 Leave a comment