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 the correct parameters. 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. We will look at the most simple, built-in, and official way: using the ReactTestUtils
from the react-dom
library.
The utilities that will allow us to simulate browser events, such as click
and change
, are in the ReactTestUtils.Simulate
module. This module contains all React-supported event types. We will try out two common ones:
ReactTestUtils.Simulate.click
ReactTestUtils.Simulate.change
Browser events are generated in the DOM, the elements that make up the markup of a web page. Because of this, the ReactTestUtils.Simulate
API requires DOM elements to operate on.
But React components generally try to ignore the DOM and operate at the component level.There is a React mechanism, however, called refs, that allows a component to access the underlying DOM.
Let's say that we have a custom component called Button
. If we pass it a special ref
prop, the value of that prop will contain a pointer to its DOM element. To allow the ref
to pass through, we must use the React.forwardRef
API in our Button.js
file:
1import React from "react";
2
3export default React.forwardRef(function Button(props, ref) {
4 return <button {...props} ref={ref} />;
5});
Now we'll set up our test code. We'll use the Jest APIs for testing that are standard in a create-react-app.Inside our test file, we'll set up to test the above Button
component.
First, we'll get our imports out of the way. Then we need to create an element for our component to render within:
1import React from "react";
2import ReactDOM from "react-dom";
3import ReactTestUtils from "react-dom/test-utils";
4
5import Button from "./Button.js";
6
7let div;
8
9beforeEach(() => {
10 div = document.createElement("div");
11});
And, since we're going to render into this div each test, we want to clean it up later as well:
1afterEach(() => {
2 ReactDOM.unmountComponentAtNode(div);
3});
Now let's test an onClick
prop on this custom Button
component.The onClick
should pass through to the component's underlying <button />
element because we're spreading ({...props}
) that props through to it.
Let's create our test:
1it("captures clicks", done => {
2 const ref = React.createRef();
3 function handleClick() {
4 done();
5 }
6 ReactDOM.render(<Button onClick={handleClick} ref={ref} />, div);
7 ReactTestUtils.Simulate.click(ref.current);
8});
In the test, we create a ref so that we can pass it to the rendered Button
and later to the Simulate.click
call.
The onClick
prop gets 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 on 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 React.forwardRef(function TextInput(props, ref) {
4 return <input {...props} ref={ref} />;
5});
Note that it's very simple, merely fronting an input
element and passing a ref
and all {...props}
into it.
The test is very similar to the last time around as well:
1it("captures changes", done => {
2 const ref = React.createRef();
3 function handleChange(evt) {
4 expect(evt.target.value).toEqual("whamo");
5 done();
6 }
7 ReactDOM.render(<TextInput onChange={handleChange} ref={ref} />, div);
8 ReactTestUtils.Simulate.change(ref.current, { target: { value: "whamo" } });
9});
Note that now we're calling the ReactTestUtils.Simulate.change
method, and we're passing a second parameter. This parameter, an object, 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 stubbbing 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 the other events we might need to in a similar fashion.
The way we've just explored to simulate and test browser events is the simplest of the options. It required no other libraries - it just uses official libraries from the React team. And it wasn't bad. We were able to complete our task without much of a hassle. Ok, ensuring refs is a little bit of a hassle.
There are many other libraries and methods for being able to simulate browser events. One of my favorites worth checking out is the React Testing Library, which gives some convenient APIs for accessing the DOM and encourages black-box approaches in our tests. To try out this library, see the related guide Simulate Browser Events in React with React Testing Library.
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.