It's been some months since I first published this article. As an attentive reader noticed the package have changed name. For latest API and installation instructions check React Testing Library
Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris
React Testing Library is a different testing library in that it tests the surface of your component rather than the internals. You can change your components as much as you want as long as they render the data the same way or the React in the same way if you after interactions such as filling in data or pressing a button for example.
This is what the author the library Kent C. Dodds says about it:
Simple and complete React DOM testing utilities that encourage good testing practices
It's a lightweight solution for testing React components. It provides utility functions on top of react-dom
. Your tests work on DOM nodes as opposed to React component instances.
In this article we will cover the following:
- Writing a test, show how simple it is to write a test, instantiate a component, and assert on it
- Dealing with events, we will learn how we can trigger event and assert on the resulting component afterward
- Asynchronous actions, we will learn how we can trigger and wait for asynchronous actions to finish
- Manage input, we will learn how to send keystrokes to input elements on our components and assert on the result
It's easy to get started, you only need to install react-testing-library
:
yarn add @testing-library/react
Writing a test
Let's look at a real scenario and see what we mean. We will create:
-
Todos.js
a component that allows you to render a list ofTodos
and select a specificTodo item
-
Todos.test.js
, our test file
Our component code looks like this:
// Todos.js
import React from 'react';
import './Todos.css';
const Todos = ({ todos, select, selected }) => (
<React.Fragment>
{todos.map(todo => (
<React.Fragment key={todo.title}>
<h3 data-testid="item" className={ selected && selected.title === todo.title ? 'selected' :'' }>{todo.title}</h3>
<div>{todo.description}</div>
<button onClick={() => select(todo)}>Select</button>
</React.Fragment>
))}
</React.Fragment>
);
class TodosContainer extends React.Component {
state = {
todo: void 0,
}
select = (todo) => {
this.setState({
todo,
})
}
render() {
return (
<Todos { …this.props } select={this.select} selected={this.state.todo} />
);
}
}
export default TodosContainer;
Now to the test:
// Todos.test.js
import {render, fireEvent, wait} from 'react-testing-library';
import 'react-testing-library/cleanup-after-each';
import React from 'react';
import 'jest-dom/extend-expect';
import Todos from '../Todos';
const todos = [
{
title: 'todo1'
},
{
title: 'todo2'
}];
describe('Todos', () => {
it('finds title', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
})
});
We can see from the above code that we are using some helpers from react-testing-library
:
- render(), this will render our component
- fireEvent, this will help us trigger things like a click event or change the input data for example
- wait, this allows us to wait for an element to appear
Looking at the test itself we see that when we call render we get an object back, and that we destructure 3 values from it:
const {getByText, getByTestId, container} = render(<Todos todos={todos} />)
and we end up with the following helpers:
- getByText, this grabs an element by it's text content
-
getByTestId, this grabs an element by
data-testid
, so if you have an attribute on your element like sodata-testid="saved"
you would be querying it like sogetByTestId('saved')
- container, the div your component was rendered to
Let's fill in that test:
// Todos.test.js
import {render, fireEvent, wait} from 'react-testing-library';
import React from 'react';
import 'jest-dom/extend-expect';
import 'react-testing-library/cleanup-after-each';
import Todos from '../Todos';
const todos = [
{
title: 'todo1'
},
{
title: 'todo2'
}];
describe('Todos', () => {
it('finds title', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
const elem = getByTestId('item');
expect(elem.innerHTML).toBe('todo1');
})
});
As we can see above, we are able to render our component and query for an h3 element by using the container and the querySelector. Finally, we assert on the text inside the element.
Handling actions
Let's have a look at our component again. Or rather let's look at an excerpt of it:
// excerpt of Todos.js
const Todos = ({ todos, select, selected }) => (
<React.Fragment>
{todos.map(todo => (
<React.Fragment key={todo.title}>
<h3 className={ selected && selected.title === todo.title ? 'selected' :'' }>{todo.title}</h3>
<div>{todo.description}</div>
<button onClick={() => select(todo)}>Select</button>
</React.Fragment>
))}
</React.Fragment>
);
We see above that we try to set the CSS class to selected
if a todo is selected. The way to get a todo
selected is to click on it, we can see how we invoke the select method when we click on the button that is rendered, one per item. Let's try to test this out by adding a test:
import {render, fireEvent, wait} from 'react-testing-library'
import React from 'react';
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import Todos from '../Todos';
const todos = [
{
title: 'todo1'
},
{
title: 'todo2'
}
];
describe('Todos', () => {
it('finds title', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
const elem = getByTestId('item');
expect(elem.innerHTML).toBe('todo1');
})
it('select todo', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
fireEvent.click(getByText('Select'));
const elem = getByTestId('item');
expect(elem.classList[0]).toBe('selected');
})
});
Our last newly added test is using the fireEvent
helper to perform a click
and we can see that we are using the getByText
helper to find the button. We again use the container to find and assert on the selected CSS class.
Asynchronous tests and working with input
We have so far shown you how to render a component, find the resulting elements, and assert on them. We have also shown how you can carry out things like a click on a button. In this section we will show two things:
- Handling input
- Dealing with asynchronous actions
We will build the following:
- Note.js, a component that allows us to enter data and save down the results, it will also allow us to fetch data
- tests/Note.js, the test file
Let's have a look at the component:
// Note.js
import React from 'react';
class Note extends React.Component {
state = {
content: '',
saved: '',
};
onChange = (evt) => {
this.setState({
content: evt.target.value,
});
console.log('updating content');
}
save = () => {
this.setState({
saved: `Saved: ${this.state.content}`,
});
}
load = () => {
var me = this;
setTimeout(() => {
me.setState({
data: [{ title: 'test' }, { title: 'test2' }]
})
}, 3000);
}
render() {
return (
<React.Fragment>
<label htmlFor="change">Change text</label>
<input id="change" placeholder="change text" onChange={this.onChange} />
<div data-testid="saved">{this.state.saved}</div>
{this.state.data &&
<div data-testid="data">
{this.state.data.map(item => (
<div className="item" >{item.title}</div>
))}
</div>
}
<div>
<button onClick={this.save}>Save</button>
<button onClick={this.load}>Load</button>
</div>
</React.Fragment>
);
}
}
export default Note;
Handling user input
To save data in our sample app, we enter text into an input and press the save button.
Let's create a test for that:
// __tests__/Note.js
import {render, fireEvent, wait} from 'react-testing-library'
import React from 'react';
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import Select from '../Note';
describe('Note', () => {
it('save text', async() => {
const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render(<Select />);
const input = getByLabelText('Change text');
input.value= 'input text';
fireEvent.change(input);
fireEvent.click(getByText('Save'));
console.log('saved', getByTestId('saved').innerHTML);
expect(getByTestId('saved')).toHaveTextContent('input text')
})
});
We can see above that we use the helper getByLabelText
to get a reference to our input and we simply do input.value = 'input text'
at that point. Then we need to invoke fireEvent.change(input)
for the change to happen. After that we can assert on the results by typing expect(getByTestId('saved')).toHaveTextContent('input text')
Dealing with asynchronous code
We have another piece of functionality in our component namely pressing a Load button that invokes a load()
method, like so:
load = () => {
var me = this;
setTimeout(() => {
me.setState({
data: [{ title: 'test' }, { title: 'test2' }]
})
}, 3000);
}
We can see above that the change doesn't happen straight away, this due to us using a setTimeout(). Having a look at our component we can see that we don't render out the data property unless it is set to a value:
{this.state.data &&
<div data-testid="data">
{this.state.data.map(item => (
<div className="item" >{item.title}</div>
))}
</div>
}
Our test needs to cater to this and wait for the div with attribute data-testid="data"
to be present before it can assert on it. This can be handled through async/await. We import waitForElement
from react-testing-library
which allows us to halt execution while waiting for the element to appear. Let's see what that looks like, by adding a test to our test file:
import {
render,
fireEvent,
wait,
waitForElement,
} from 'react-testing-library'
import 'react-testing-library/cleanup-after-each';
import React from 'react';
import 'jest-dom/extend-expect'
import Select from '../Note';
describe('Note', () => {
it('save text', async () => {
const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render(<Select />);
const input = getByLabelText('Change text');
input.value= 'input text';
fireEvent.change(input);
fireEvent.click(getByText('Save'));
console.log('saved', getByTestId('saved').innerHTML);
expect(getByTestId('saved')).toHaveTextContent('input text')
})
it('load data', async() => {
const {getByText, getByTestId, getByPlaceholderText, container} = render(<Select />);
fireEvent.click(getByText('Load'));
const elem = await waitForElement(() => getByTestId('data'))
const elem = getByTestId('item');
expect(elem).toHaveTextContent('test');
})
});
Above we see the construct await waitForElement(() => getByTestId('data'))
that prevent the test from continuing until the element is present. The waitForElement
returns a promise that doesn't resolve until the element exist on the DOM. Thereafter we assert on the result.
Summary
We took a look at react-testing-library and wrote tests covering core use-cases. We learned how to deal with events, asynchronous actions, how to manage user input. We covered most things this library has to offer but more importantly, we learned how to think about testing in a different way.
Maybe we don't have to test the internals but rather the surface of our components?
Further reading
There is a lot more to this library and you are encouraged to look at the
- Official documentation at Repository
- Blog Post by its creator Kent C Dodd's Blog post
Top comments (2)
Isn't it they changed package name to @testing-library/react?
Thanks for noticing. Yea it was more than 6 months ago I wrote this article.. I've updated the article, thanks again