c
Testing React apps
There are many different ways of testing React applications. Let's take a look at few of those next.
Tests will be implemented with the same Jest testing library developed by Facebook that was used in the previous part.
In addition to Jest, we also need another testing library that will help us render components for testing purposes. The option we'll use for this class is react-testing-library, which had seen rapid growth in popularity in recent times.
Let's install libraries with the command:
npm i -D @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom @babel/preset-env @babel/preset-react
The file package.json should be extended as follows:
{
"scripts": {
// ...
"test": "jest"
}
// ...
"jest": {
"testEnvironment": "jsdom"
}
}
We also need the file .babelrc with the following content:
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }]
]
}
Before we start writing tests, let's review the Task
component and what it renders:
const Task = ({ task, toggleImportance }) => {
const label = task.important
? "make not important"
: "make important";
return (
<li className='task'> {task.content}
<button onClick={toggleImportance}>{label}</button>
</li>
);
};
Notice that the li
element has the CSS classname task
,
which could be used to access the component in our tests.
Rendering the component for tests
We will write our first test in the src/components/Task.test.js file, which is in the same directory as the component itself.
The first test verifies that the component renders the contents of the task:
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import Task from "./Task";
test("renders content", () => {
const task = {
content: "Component testing is done with react-testing-library",
important: true
};
render(<Task task={task} />); const element = screen.getByText("Component testing is done with react-testing-library"); expect(element).toBeDefined();});
Before we run the test, let's examine the contents of the test itself.
After some initial configuration, our newest test renders the component with the
render
method
provided by the react-testing-library:
render(<Task task={task} />);
Normally React components are rendered to the DOM.
The render
method we used renders the components in a format that is suitable for tests without rendering them to the DOM.
We can use the screen
object to access the rendered component.
We use screen
's method getByText
to search for an element that has the task content and ensure that it exists:
const element = screen.getByText("Component testing is done with react-testing-library");
expect(element).toBeDefined();
Running tests
Run the test with command npm test
:
$ npm test
> tasks@0.0.0 test
> jest
PASS src/components/Task.test.js
✓ renders content (15 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.169 s
Ran all test suites
As expected, the test passes.
FYI: If you did not observe any errors in the terminal, you can skip this small section. The console may issue a warning if you have not installed Watchman. Watchman is an application developed by Facebook that watches for changes that are made to files. The program speeds up the execution of tests and at least starting from macOS Sierra, running tests in watch mode issues some warnings to the console, which can be removed by installing Watchman.
You can install Watchman using your package manager of choice
Test file location
In React there are (at least) two different conventions for the test file's location. We created our test files according to the current standard by placing them in the same directory as the component being tested.
The other convention is to store the test files in a separate test directory. This debate on which convention is better is almost as contentious (and hilarious IMO) as the debate on whether you use Tabs or Spaces for indentation.
I prefer to store tests and application code separately. Nonetheless, we'll keep them in the same folder because some of the previous libraries we've used follow this convention by default.
Searching for content in a component
The react-testing-library package offers many different ways of investigating the content of the component being tested.
In reality, the expect
in our test is not needed at all!
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import Task from "./Task";
test("renders content", () => {
const task = {
content: "Component testing is done with react-testing-library",
important: true
};
render(<Task task={task} />);
const element = screen.getByText("Component testing is done with react-testing-library");
expect(element).toBeDefined();});
The test fails if, on the previous line, getByText
does not find the element it is looking for.
We could also use CSS selectors
to find rendered elements by using the querySelector
method
on the container
object that is one of the fields returned by the render:
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import Task from "./Task";
test("renders content", () => {
const task = {
content: "Component testing is done with react-testing-library",
important: true
};
const { container } = render(<Task task={task} />);
const div = container.querySelector(".task"); expect(div).toHaveTextContent( "Component testing is done with react-testing-library" );});
Pertinent: A more consistent way of selecting elements is using a data attribute that is specifically defined for testing purposes. Using react-testing-library, we can leverage the
getByTestId
method to select elements with a specifieddata-testid
attribute.
Debugging tests
We typically run into many different kinds of problems when writing our tests.
Object screen
has the method debug
that can be used to print the HTML of a component to the terminal.
If we change the test as follows:
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import Task from "./Task";
test("renders content", () => {
const task = {
content: "Component testing is done with react-testing-library",
important: true
};
const { container } = render(<Task task={task} />);
screen.debug();
// ...
});
the HTML gets printed to the console:

It is also possible to use the same method to print a wanted element to the console:
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import Task from "./Task";
test("renders content", () => {
const task = {
content: "Component testing is done with react-testing-library",
important: true
};
const { container } = render(<Task task={task} />);
const element = screen.getByText("Component testing is done with react-testing-library");
screen.debug(element);
// ...
});
Now the HTML of the wanted element gets printed:
<li
class="task"
>
Component testing is done with react-testing-library
<button>
make not important
</button>
</li>
Clicking buttons in tests
In addition to displaying content, the Task
component also makes sure that when the button associated with the task is pressed,
the toggleImportance
event handler function gets called.
Let us install a library called user-event that makes simulating user input a bit easier:
npm i -D @testing-library/user-event
Testing this functionality can be accomplished by adding another test like this:
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";import Task from "./Task";
// ...
test("clicking the button calls event handler once", async () => {
const task = {
content: "Component testing is done with react-testing-library",
important: true
};
const mockHandler = jest.fn();
render(
<Task task={task} toggleImportance={mockHandler} /> );
const user = userEvent.setup(); const button = screen.getByText("make not important"); await user.click(button);
expect(mockHandler.mock.calls).toHaveLength(1);});
There are a few interesting things related to this test. The event handler is a mock function defined with Jest:
const mockHandler = jest.fn();
A session is started to interact with the rendered component:
const user = userEvent.setup();
The test finds the button based on the text from the rendered component and clicks the element:
const button = screen.getByText("make not important");
await user.click(button);
Clicking happens via the click
method from the userEvent library.
The expectation of the test verifies that the mock function has been called exactly once.
expect(mockHandler.mock.calls).toHaveLength(1);
Mock objects and functions are commonly used stub components in testing that are used for replacing dependencies of the components being tested. Mocks make it possible to return hardcoded responses, and to verify the number of times the mock functions are called and with what parameters.
In our example, the mock function is a perfect choice since it can be easily used for verifying that the method gets called exactly once.
Tests for the Togglable
component
Let's write a few tests for the Togglable
component.
Let's add the togglableContent
CSS classname to the div
that returns the child components in components/Togglable.jsx.
const Togglable = forwardRef((props, ref) => {
// ...
return (
<div>
<div style={hideWhenVisible}>
<button onClick={toggleVisibility}>
{props.buttonLabel}
</button>
</div>
<div style={showWhenVisible} className="togglableContent"> {props.children}
<button onClick={toggleVisibility}>cancel</button>
</div>
</div>
);
});
The tests that we added in a new file Togglable.test.js are shown below:
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Togglable from "./Togglable";
describe("<Togglable />", () => {
let container;
beforeEach(() => {
container = render(
<Togglable buttonLabel="show...">
<div className="testDiv" >
togglable content
</div>
</Togglable>
).container;
});
test("renders its children", async () => {
await screen.findAllByText("togglable content");
});
test("at start the children are not displayed", () => {
const div = container.querySelector(".togglableContent");
expect(div).toHaveStyle("display: none");
});
test("after clicking the button, children are displayed", async () => {
const user = userEvent.setup();
const button = screen.getByText("show...");
await user.click(button);
const div = container.querySelector(".togglableContent");
expect(div).not.toHaveStyle("display: none");
});
});
The beforeEach
function gets called before each test, which then renders the Togglable
component and saves the field container
of the return value.
The first test verifies that the Togglable
component renders its child component
<div className="testDiv">
togglable content
</div>
The remaining tests use the toHaveStyle
method
to verify that the child component of the Togglable
component is not visible initially,
by checking that the style of the div
element contains { display: 'none' }
.
Another test verifies that when the button is pressed the component is visible,
meaning that the style for hiding the component is no longer assigned to the component.
Let's also add a test that can be used to verify that the visible content can be hidden by clicking the second button of the component:
describe("<Togglable />", () => {
// ...
test("toggled content can be closed", async () => {
const user = userEvent.setup();
const button = screen.getByText("show...");
await user.click(button);
const closeButton = screen.getByText("cancel");
await user.click(closeButton);
const div = container.querySelector(".togglableContent");
expect(div).toHaveStyle("display: none");
});
});
Testing the forms
We already used the click
function of the user-event in our previous tests to click buttons.
const user = userEvent.setup();
const button = screen.getByText("show...");
await user.click(button);
We can also simulate text input with userEvent
.
Let's make a test for the TaskForm
component in a separate file.
But before we do, let's review and make a small change to our TaskForm
component.
import { useState } from "react";
const TaskForm = ({ createTask }) => {
const [newTask, setNewTask] = useState("");
const handleChange = (event) => {
setNewTask(event.target.value);
};
const addTask = (event) => {
event.preventDefault();
createTask({
content: newTask,
important: Math.random() > 0.5,
});
setNewTask("");
};
return (
<div className="formDiv"> <h2>Create a new task</h2>
<form onSubmit={addTask}>
<input
value={newTask}
onChange={handleChange}
/>
<button type="submit">save</button>
</form>
</div>
);
};
export default TaskForm;
Notice that we added the formDiv
CSS class, similar to what we did with our Togglable
component above.
Also, recall that TaskForm
works by calling the createTask
function it received as props with the new task's details.
We are now ready to write our test. Taskform.test.js is as follows:
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import TaskForm from "./TaskForm";
import userEvent from "@testing-library/user-event";
test("<TaskForm /> updates parent state and calls onSubmit", async () => {
const createTask = jest.fn();
const user = userEvent.setup();
render(<TaskForm createTask={createTask} />);
const input = screen.getByRole("textbox");
const sendButton = screen.getByText("save");
await user.type(input, "testing a form...");
await user.click(sendButton);
expect(createTask.mock.calls).toHaveLength(1);
expect(createTask.mock.calls[0][0].content).toBe("testing a form...");
});
Tests get access to the input field using the function getByRole
.
The method type
of the userEvent
is used to write text to the input field.
Our first expect
call ensures that submitting the form calls the createTask
method.
The second expectation checks that the event handler is called with the right parameters.
In other words, the second expect
checks that a task with the correct content is created when the form is filled.
About finding the elements
Let us assume that the form has two input fields
const TaskForm = ({ createTask }) => {
// ...
return (
<div>
<h2>Create a new task</h2>
<form onSubmit={addTask}>
<input
value={newTask}
onChange={handleChange}
/>
<input value={...} onChange={...} /> <button type="submit">save</button>
</form>
</div>
);
};
Now the approach that our test uses to find the input field
const input = screen.getByRole("textbox");
would cause an error:

The error message suggests using getAllByRole
.
The test could be fixed as follows:
const inputs = screen.getAllByRole("textbox");
await user.type(inputs[0], "testing a form...");
Method getAllByRole
now returns an array and the right input field is the first element of the array.
However, this approach is brittle since it relies on the order of the input fields.
Quite often input fields have some placeholder text that provides hints to the user about what kind of input is expected. Let us add a placeholder to our form:
const TaskForm = ({ createTask }) => {
// ...
return (
<div>
<h2>Create a new task</h2>
<form onSubmit={addTask}>
<input
value={newTask}
onChange={handleChange}
placeholder='write task here' />
<input
value={...}
onChange={...}
/>
<button type="submit">save</button>
</form>
</div>
);
};
Now finding the right input field is easy with the method getByPlaceholderText
:
test("<TaskForm /> updates parent state and calls onSubmit", () => {
const createTask = jest.fn();
render(<TaskForm createTask={createTask} />);
const input = screen.getByPlaceholderText("write task here"); const sendButton = screen.getByText("save");
userEvent.type(input, "testing a form...");
userEvent.click(sendButton);
expect(createTask.mock.calls).toHaveLength(1);
expect(createTask.mock.calls[0][0].content).toBe("testing a form...");
});
The most flexible way of finding elements in tests is the method querySelector
of the container
object, which is returned by render
,
as was mentioned earlier in this part.
Any CSS selector can be used with this method for searching elements in tests.
For example, if we added a unique id
to the input field:
const TaskForm = ({ createTask }) => {
// ...
return (
<div>
<h2>Create a new task</h2>
<form onSubmit={addTask}>
<input
value={newTask}
onChange={handleChange}
id='task-input' />
<input
value={...}
onChange={...}
/>
<button type="submit">save</button>
</form>
</div>
);
};
The input element could now be found in the test as follows:
const { container } = render(<TaskForm createTask={createTask} />);
const input = container.querySelector("#task-input");
However, we shall stick to the approach of using getByPlaceholderText
in the test.
Let us look at a couple of details before moving on. Assume that a component would render text to an HTML element as follows:
const Task = ({ task, toggleImportance }) => {
const label = task.important
? "make not important" : "make important";
return (
<li className='task'>
Your awesome task: {task.content} <button onClick={toggleImportance}>{label}</button>
</li>
);
};
export default Task;
the getByText
command that the test uses does not find the element
test("renders content", () => {
const task = {
content: "Does not work anymore :(",
important: true
};
render(<Task task={task} />);
const element = screen.getByText("Does not work anymore :(");
expect(element).toBeDefined();
});
Command getByText
looks for an element that has the same text that it has as a parameter, and nothing more.
If we want to look for an element that contains the text, we could use an extra option:
const element = screen.getByText(
"Does not work anymore :(", { exact: false }
);
or we could use the command findByText
:
const element = await screen.findByText("Does not work anymore :(");
Notice that, unlike the other ByText
commands, findByText
returns a promise!
There are situations where yet another form of the command queryByText
is useful.
queryByText
returns the element but it does not cause an exception if the element is not found.
We could use the command to ensure that something is not rendered to the component:
test("does not render this", () => {
const task = {
content: "This is a reminder",
important: true
};
render(<Task task={task} />);
const element = screen.queryByText("do not want this thing to be rendered");
expect(element).toBeNull();
});
Test coverage
We can easily find out the coverage of our tests by running them with the command.
npm test -- --coverage --collectCoverageFrom='src/**/*.{jsx,js}'

A no-frills HTML report will be generated to the coverage/lcov-report directory. The report will tell us the lines of untested code in each component:

You can find the code for our current application in its entirety in the part5-8 branch of this GitHub repository.
Frontend integration tests
In the previous part of the course material, we wrote integration tests for the backend that tested its logic and connected the database through the API provided by the backend. When writing these tests, we made the conscious decision not to write unit tests, as the code for that backend is fairly simple, and it is likely that bugs in our application occur in more complicated scenarios than what would be discovered via our unit tests.
So far all of our tests for the frontend have been unit tests that have validated the correct functioning of individual components. Unit testing is useful at times, but even a comprehensive suite of unit tests is not enough to validate that the application works as a whole.
We could also make integration tests for the frontend. Integration testing tests the collaboration of multiple components. It is considerably more difficult than unit testing. For example, we would have to mock data from the server. We will concentrate on making end-to-end tests to test the whole application. We will work on the end-to-end tests in the next section.
Snapshot testing
Jest offers a completely different alternative to "traditional" testing called snapshot testing. The interesting feature of snapshot testing is that developers do not need to define any tests themselves, it is simple enough to adopt snapshot testing.
The fundamental principle is to compare the HTML code defined by the component after it has changed to the HTML code that existed before it was changed.
If the snapshot notices some change in the HTML defined by the component, then *either it is new functionality or a bug caused by accident. Snapshot tests notify the developer if the HTML code of the component changes. The developer has to tell Jest if the change was desired or undesired. *If the change to the HTML code is unexpected, it strongly implies a bug, and the developer can become aware of these potential issues easily thanks to snapshot testing.