When unit testing React components, it's often useful to be able to simulate browser events. This enables us to assert that event handlers are called and assert correct arguments. We simulate browser events because, often, we don't want to require a real browser in our test environment, especially in our unit tests.
There are many different ways to simulate browser events. Here, we will look at a solid third-party library that gives us a great API and an approach for testing browser events, React Testing Library.
The utilities that will allow us to simulate browser events, such as click
and change
, are in the React Testing Library's fireEvent
module. This module contains many different supported events. We will try out two common ones:
fireEvent.click
fireEvent.change
To use the library, first install it:
1npm install @testing-library/react --save-dev
To have something to test, let's set up a simple component:
1import React from "react";
2
3export default function Button(props) {
4 return <button {...props} />;
5}
There's not much of anything going on here. We're creating the simplest possible custom component and spreading props onto a button
element.
Let's test an onClick
prop on this custom Button
component. We'll use the jest APIs that are standard in create-react-app:
1import {
2 cleanup,
3 fireEvent,
4 render
5} from "@testing-library/react";
6
7import Button from "./Button.js";
8
9it("captures clicks", done => {
10 function handleClick() {
11 done();
12 }
13 const { getByText } = render(
14 <Button onClick={handleClick}>Click Me</Button>
15 );
16 const node = getByText("Click Me");
17 fireEvent.click(node);
18});
19
20afterEach(cleanup);
First, we import the needed functions from @testing-library/react
.
In our test, we call render
from that library, passing in the usage of our Button
component. React Testing Library handles setting up the DOM for test then rendering into that DOM. To make sure that DOM is getting cleaned up between tests, so each can run independently, we call afterEach(cleanup)
.
The render
function call returns many things. In this example, we're deconstructing one of React Testing Library's getBy
functions, specifically getByText
. This is a function that will search in the rendered DOM of this specific test for the element that has matching innerText. We look for "Click Me"
, in the example.
Once we have this node, we're able to dispatch the click event on it with fireEvent.click
.
The onClick
prop to <Button />
is the value of a handleClick
event handler function. That function calls done()
, which is the callback to the it
function`.
Note that passing this callback makes the test async, meaning it will not complete until the callback is called. If it is never called, the test will time out, failing. Conversely, if it is called, it will indicate that the click was simulated on the button and the handler is called. And with the above code, it is!
Let's try another kind of browser event simulation. This time, we'll simulate a change event. This kind of event is useful for something like a text input field, where we want to know when the input value changes and what the current value actually is.
We'll create a TextInput.js
component:
1import React from "react";
2
3export default function TextInput(props) {
4 return <input {...props} />;
5}
Note that it's, again, very simple, merely fronting an input
element passing all the {...props}
into it.
The test is very similar to the last time around, as well:
1it("captures changes", done => {
2 function handleChange(evt) {
3 expect(evt.target.value).toEqual("whamo");
4 done();
5 }
6 const { getByPlaceholderText } = render(
7 <TextInput onChange={handleChange} placeholder="Change Me" />
8 );
9 const node = getByPlaceholderText("Change Me");
10 fireEvent.change(node, { target: { value: "whamo" } });
11});
This time, to find the node to fire events on, we're using a different query function, getByPlaceholderText
. And, we're calling the fireEvent.change
function, passing a new, second parameter. This object we pass as an argument represents the Event object. For instance, it includes the target
property, usually indicating the element that the Event was dispatched from. Here, we're simply stubbing a value.
Then, in our handleChange
handler, we can assert the value of the event target to come through as "whamo"
(always a great input value).
We've now simulated two different browser events. We should be able to handle any other events we might need to in a similar fashion.
This is a solid community library that gives us some great APIs for event simulation and other things. The query methods that we've used have also helped us not reach into our component implementation but rather treat it as a black box, inspecting the DOM itself (the things that a normal client of our component would be able to see).
There are many other libraries and methods for being able to simulate browser events. One of the most basic, not requiring any additional testing library like this is using the official react-dom/test-utils
. To explore this approach, see the related guide Simulate Browser Events in React with ReactTestUtils.
Experiment and try a few approaches, then choose one appropriate to your tastes and project.
To run the tests in the above examples on your own machine, check out this demo-react-simulate-test project.