React-testing-library: fireEvent vs userEvent

November 20, 2021

The black sheep

When we develop our React applications there is a part which is as important as the development itself, and which sometimes is left behind: the testing.

With an appropriate testing we can adjust and change our code knowing that our application (or our components) will continue working as expected since, otherwise, our tests will warn us about it.

We can discuss wether we should follow a TDD or a BDD approach, or if we should develop first and test later, or maybe a little bit of this and that because we are non-conformist people and we like to do it our own way breaking with everything pre-established; but there is something that everyone agrees with: having tools that make your life easier is always better.

React-testing-library: testing from user experience

Here is where we find react-testing-library (or @testing-library/react, as its package name).

This library will help us to approach our testing from a user experience perspective, as it will give us tools not to test the implementation details of the component, but to test the user behaviour. It will help us to make our testing thinking in the user and what they will see and will experience in the end as the result of any interaction.

If we want to interact with our components and elements in our tests we should be capable to simulate those interactions, and that's the reason why this library includes the fireEvent tool, which will allow us to trigger events in our DOM elements and simulate the events that the user would trigger in their interactions.

import { fireEvent } from "@testing-library/react";

Inside this fireEvent object, we can find some methods with which we'll be able to trigger events programatically, such as click(), mouseOver(), focus(), change()...

But if we want to take full advantage of this kind of tests and we really want to simulate what the user would do and how our application will react to those user interactions, we need something else.

fireEvent vs userEvent

Here is where we can use user-event (or @testing-library/user-event, as its package name).

import userEvent from "@testing-library/user-event";

This library will allow us, same as fireEvent, to trigger some events in our DOM elements, so we can simulate those interactions and check how our application reacts to them.

So, what's the difference between them? Well, the difference is that userEvent simulates complete interactions, and not only single events. We can find inside the userEvent object events such as click(), type(), hover(), clear()...

But what does it mean to say that it simulates complete interactions? We can have a better understanding if we take a look at its source code:

function click(...) {
  if (!skipPointerEventsCheck && !hasPointerEvents(element)) {
    throw new Error(...)
  }
  // We just checked for `pointerEvents`. We can always skip this one in `hover`.
  if (!skipHover) hover(element, init, {skipPointerEventsCheck: true})

  if (isElementType(element, 'label')) {
    clickLabel(element, init, {clickCount})
  } else if (isElementType(element, 'input')) {
    if (element.type === 'checkbox' || element.type === 'radio') {
      clickBooleanElement(element, init, {clickCount})
    } else {
      clickElement(element, init, {clickCount})
    }
  } else {
    clickElement(element, init, {clickCount})
  }
}

In this piece of code, we can see how the userEvent library uses fireEvent methods to trigger some events, but it calls them in sequence and in the order in which they would occur if a real user was the one behind the screen.

In this case, the click() method won't only click the element, but it will also simulate the previous hover that a real user would trigger moving the pointer over the element to click it.

If we follow the traces, we'll see how the clickElement() method which is being called is nothing else than a set of fireEvent methods triggered one after the other, simulating a pointerDown and a pointerUp later, among the rest.

We can see a very clear example explained in the userEvent official documentation.

Testing the theory

Maybe there are some doubts on how this affects us, but we can test it with a little React project and some very simple tests so we can see the differences between these two libraries.

To create the React project, we can follow these simple steps, which include also Prettier and Eslint installation so we keep good practices in our code, even if it's just a test project!

Create our project with CRA

> npx create-react-app testing-library-project

Install eslint y prettier

> yarn add -D eslint-config-airbnb eslint-config-prettier eslint-plugin-jsx-a11y eslint-plugin-prettier prettier

Create .eslintrc file in our root

After creating our file with this content, we'll also delete the "eslintConfig" key in our package.json to avoid conflicts:

{
  "extends": [
    "react-app",
    "react-app/jest",
    "airbnb",
    "plugin:jsx-a11y/recommended",
    "prettier"
  ],
  "plugins": ["jsx-a11y", "prettier"],
  "rules": {
    "jsx-a11y/mouse-events-have-key-events": 0
  }
}

Optional: create .eslintignore file to avoid errors

We can see some eslint errors in our console due to some packages installed in our folder node_modules. To avoid them, simply create an .eslintignore file and add this content:

node_modules

Add user-event and dom libraries

> yarn add -D @testing-library/user-event @testing-library/dom

Preparing the battlefield

Due to our newly added eslint rules, we'll have to do some changes in our files to fit the requirements and remove the errors and warnings. Once done, we'll modify our App.jsx file so that its content looks like this:

import React, { useState } from "react";
import "./App.css";

function App() {
  const [state, setState] = useState({
    onHoverActivated: false,
    onClickActivated: false,
    onTypeActivated: false,
    onFocusActivated: false,
  });
  const {
    onHoverActivated,
    onClickActivated,
    onTypeActivated,
    onFocusActivated,
  } = state;

  const handleOnHover = () => {
    setState((current) => ({ ...current, onHoverActivated: true }));
  };

  const handleOnClick = () => {
    setState((current) => ({ ...current, onClickActivated: true }));
  };

  const handleOnType = () => {
    setState((current) => ({ ...current, onTypeActivated: true }));
  };

  const handleOnFocus = () => {
    setState((current) => ({ ...current, onFocusActivated: true }));
  };

  return (
    <div className="App">
      <section>
        <div>
          <button
            type="button"
            data-testid="button-target"
            onClick={handleOnClick}
            onMouseOver={handleOnHover}
          >
            This will have hover and click events assigned!
          </button>
          <span data-testid="on-hover-span">{onHoverActivated.toString()}</span>
          <span data-testid="on-click-span">{onClickActivated.toString()}</span>
        </div>
      </section>
      <section>
        <div>
          <input
            type="text"
            onFocus={handleOnFocus}
            onKeyDown={handleOnType}
            data-testid="input-target"
          />
          <span data-testid="on-type-span">{onTypeActivated.toString()}</span>
          <span data-testid="on-focus-span">{onFocusActivated.toString()}</span>
        </div>
      </section>
    </div>
  );
}

export default App;

As we can see, we're only creating a button and a text input with some event handlers associated to them, and we've added some span elements which will contain our variables' values.

Let's begin.

Testing the differences

We'll modify our App.test.jsx file to add the needed import statements for our tests:

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "./App";

If we want, we can include a little helper that renders our component in each test, and we can create an initial test so we can check if everything is in place, wrapping everything around a describe block for the sake of organisation:

describe("App container", () => {
  const renderApp = () => render(<App />);

  it("renders the app", () => {
    renderApp();
    expect(screen.getByTestId("on-hover-span")).toBeInTheDocument();
  });
});

Now we should be able to run yarn test in our consoles and everything should be in green.

Checking interactions with our button

We will create now the tests for our button, so we'll use again the describe block:

describe("button control", () => {
  let onClickValue;
  let onHoverValue;

  it("clicks the button using fireEvent", () => {
    renderApp();
    fireEvent.click(screen.getByTestId("button-target"));

    expect(screen.getByTestId("on-click-span")).toHaveTextContent(onClickValue);
    expect(screen.getByTestId("on-hover-span")).toHaveTextContent(onHoverValue);
  });

  it("clicks the button using userEvent", () => {
    renderApp();
    userEvent.click(screen.getByTestId("button-target"));

    expect(screen.getByTestId("on-click-span")).toHaveTextContent(onClickValue);
    expect(screen.getByTestId("on-hover-span")).toHaveTextContent(onHoverValue);
  });
});

This test is going to fail, of course, because in our document will appear true or false as the content of these span elements, and we've not assigned any value to those variables yet. Having into account what we've already seen, which values do you think our onClickValue and onHoverValue variables will have?

As we've explained, fireEvent will only trigger a specific event, so in the first test, the values will equal true and false, respectively. Only the click event has been triggered, and nothing else.

In the second test, however, using userEvent we're simulating a complete interaction from the user, so both values will be true: the library will simulate that the user is moving the mouse until it's over the button, and they press and release the mouse button.

In this case, if we had used event handlers for mouseUp and mouseMove events, they would have also their values set to true.

Checking interactions with our input

We will create our tests for our input interactions:

describe("input control", () => {
  let onTypeValue;
  let onFocusValue;

  it("changes the input value using fireEvent", () => {
    renderApp();
    const input = screen.getByTestId("input-target");
    fireEvent.change(input, { target: { value: "hello" } });

    expect(input).toHaveDisplayValue("hello");
    expect(screen.getByTestId("on-type-span")).toHaveTextContent(onTypeValue);
    expect(screen.getByTestId("on-focus-span")).toHaveTextContent(onFocusValue);
  });

  it("changes the input value using userEvent", () => {
    renderApp();
    const input = screen.getByTestId("input-target");
    userEvent.type(input, "hello");

    expect(input).toHaveDisplayValue("hello");
    expect(screen.getByTestId("on-type-span")).toHaveTextContent(onTypeValue);
    expect(screen.getByTestId("on-focus-span")).toHaveTextContent(onFocusValue);
  });
});

In this case, which value do you think our onTypeValue and onFocusValue variables will have? As we've seen previously, fireEvent will only trigger the onChange event, so both of them will be false in the first test. Keep in mind that in our application, we are listening for onFocus and onKeyDown events, but none of them were triggered.

On the other hand, in the second case, using userEvent, both values will be true, because we're simulating a typing interaction, and the library will execute onFocus and onKeyDown events, which will also trigger the onChange event in our input element.

Which to use and when to use it?

The answer to this question is easy: we should always try to use userEvent over fireEvent whenever we are able to, except in very specific situations.

Those exepctions can be scenarios in which some of those events inside the interaction chain make impossible to test correctly the logic we want to test.

For example, if we want to test if our input doesn't execute the onChange method if it has not been focused, using userEvent will trigger the onFocus event, which will make it impossible for us to test that behaviour. In this case, fireEvent would be the chosen one so we can trigger only and specifically that event.

But this approach corresponds to a more logical focused testing, and that is not useful for our end users, as they won't be able to see nor experiment these kind of behaviours. If we want to test really how our user will see and use our application, this is not the correct methodology.

Because of this, userEvent should be the library of choice in those tests that require to simulate interactions with our components and DOM elements.

About the author: Albert Nevado
Comments
Join us