The Complete Beginner's Guide to Testing React Apps
Tags: react, testing, javascript, beginners
Published: 2019-10-09 16:38:04

Building web applications is not an easy task as of today. To do so, you're probably using something like React, Vue, or Angular. Your app is faster, the code is both more maintainable and readable. But that's not enough. The more your codebase grows, the more complex and buggy it is. So if you care about that, **learn to write tests**. That's what we'll do today for React apps.

Luckily for you, there are already testing solutions for React, especially one: [react-testing-library](https://github.com/testing-library/react-testing-library) made by [Kent C. Dodds](https://kentcdodds.com/). So, let's discover it, shall we?

## Why React Testing Library

Basically, React Testing Library (RTL) is made of simple and complete React DOM testing utilities that encourage good testing practices, especially one:

> The more your tests resemble the way your software is used, the more confidence they can give you. - [Kent C. Dodds](https://twitter.com/kentcdodds/status/977018512689455106)

In fact, developers tend to test what we call **implementation details**. Let's take a simple example to explain it. We want to create a counter that we can both increment and decrement. Here is the implementation (with a class component) with two tests: the first one is written with [Enzyme](https://github.com/airbnb/enzyme) and the other one with React Testing Library.

```jsx
// counter.js
import React from "react"

class Counter extends React.Component {
  state = { count: 0 }
  increment = () => this.setState(({ count }) => ({ count: count + 1 }))
  decrement = () => this.setState(({ count }) => ({ count: count - 1 }))
  render() {
    return (
      <div>
        <button onClick={this.decrement}>-</button>
        <p>{this.state.count}</p>
        <button onClick={this.increment}>+</button>
      </div>
    )
  }
}

export default Counter
```

```jsx
// counter-enzyme.test.js
import React from "react"
import { shallow } from "enzyme"

import Counter from "./counter"

describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    const wrapper = shallow(<Counter />)
    expect(wrapper.state("count")).toBe(0)

    wrapper.instance().increment()
    expect(wrapper.state("count")).toBe(1)

    wrapper.instance().decrement()
    expect(wrapper.state("count")).toBe(0)
  })
})
```

```jsx
// counter-rtl.test.js
import React from "react"
import { fireEvent, render, screen } from "@testing-library/react"

import Counter from "./counter"

describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    render(<Counter />)
    const counter = screen.getByText("0")
    const incrementButton = screen.getByText("+")
    const decrementButton = screen.getByText("-")

    fireEvent.click(incrementButton)
    expect(counter.textContent).toEqual("1")

    fireEvent.click(decrementButton)
    expect(counter.textContent).toEqual("0")
  })
})
```

**Note**: Don't worry if you don't fully understand the test files. We'll see all of this afterward 😉

Can you guess which test file is the best one and why? If you're not used to tests, you may think that both are fine. In fact, the two tests make sure that the counter is incremented and decremented. However, the first one is testing implementation details, and it has two risks:

- **false-positive**: the test passes even if the code is broken.
- **false-negative**: the test is broken even if the code is right.

### False-positive

Let's say we want to refactor our components because we want to make it possible to set any count value. So we remove our `increment` and `decrement` methods and then add a new `setCount` method. We forgot to wire this new method to our different buttons:

```jsx
// counter.js
import React from "react"

class Counter extends React.Component {
  state = { count: 0 }
  setCount = (count) => this.setState({ count })
  render() {
    return (
      <div>
        <button onClick={this.decrement}>-</button>
        <p>{this.state.count}</p>
        <button onClick={this.increment}>+</button>
      </div>
    )
  }
}

export default Counter
```

The first test (Enzyme) will pass, but the second one (RTL) will fail. Indeed, the first one doesn't care if our buttons are **correctly wired** to the methods. It just looks at the implementation itself: our `increment` and `decrement` method. This is a **false positive.**

### False-negative

Now, what if we wanted to refactor our class component to hooks? We would change its implementation:

```jsx
// counter.js
import React, { useState } from "react"

const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount((count) => count + 1)
  const decrement = () => setCount((count) => count - 1)
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>{count}</p>
      <button onClick={increment}>+</button>
    </div>
  )
}

export default Counter
```

This time, the first test is going to be broken even if your counter still works. This is a **false-negative**! Enzyme will complain about `state` not being able to work on functional components:

```shell
ShallowWrapper::state() can only be called on class components
```

Then we have to change the test:

```jsx
import React from "react";
import { shallow } from "enzyme";

import Counter from "./counter";

describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    const setValue = jest.fn();
    const useStateSpy = jest.spyOn(React, "useState");
    useStateSpy.mockImplementation((initialValue) => [initialValue, setValue]);
    const wrapper = shallow(<Counter />);

    wrapper.find("button").last().props().onClick();
    expect(setValue).toHaveBeenCalledWith(1);
    // We can't make any assumptions here on the real count displayed
    // In fact, the setCount setter is mocked!

    wrapper.find("button").first().props().onClick();
    expect(setValue).toHaveBeenCalledWith(-1);
  });
});
```

To be honest, I'm not even sure if this is the right way to test it with Enzyme when it comes to hooks. In fact, we can't even make assumptions on the displayed count because of the mocked setter.

However, the test without implementation details works as expected in all cases! If we had something to retain so far, it would be to **avoid testing implementation details**.

**Note**: I'm not saying Enzyme is bad. I'm just saying testing implementation details will make tests harder to maintain and unreliable. In this article, we are going to use React Testing Library because it encourages testing best practices.

## A simple test step-by-step

Maybe there is still an air of mystery around the test written with React Testing Library. As a reminder, here it is:

```jsx
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

import Counter from "./counter";

describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    render(<Counter />);
    const counter = screen.getByText("0");
    const incrementButton = screen.getByText("+");
    const decrementButton = screen.getByText("-");

    fireEvent.click(incrementButton);
    expect(counter.textContent).toEqual("1");

    fireEvent.click(decrementButton);
    expect(counter.textContent).toEqual("0");
  });
});
```

Let's decompose it to understand how they're made of. Introducing the **AAA** pattern: **Arrange, Act, Assert**.

```jsx
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

import Counter from "./counter";

describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    // Arrange
    render(<Counter />);
    const counter = screen.getByText("0");
    const incrementButton = screen.getByText("+");
    const decrementButton = screen.getByText("-");

    // Act
    fireEvent.click(incrementButton);
    // Assert
    expect(counter.textContent).toEqual("1");

    // Act
    fireEvent.click(decrementButton);
    // Assert
    expect(counter.textContent).toEqual("0");
  });
});
```

Almost of your tests will be written that way:

1. You **arrange** (= setup) your code so that everything is ready for the next steps.
2. You **act**, you perform the steps a user is supposed to do (such as a click).
3. You make **assertions** on what is supposed to happen.

### Arrange

In our test, we've done two tasks in the arrange part:

1. Render the component
2. Getting the different elements of the DOM needed using queries and `screen`

#### Render

We can render our component with the `render` method, which is part of RTL's API:

```jsx
function render(
  ui: React.ReactElement,
  options?: Omit<RenderOptions, 'queries'>
): RenderResult
```

Where `ui` is the component to mount. We can provide some options to `render`, but they are not often needed so, I'll let you check out what's possible [in the docs](https://testing-library.com/docs/react-testing-library/api#render-options).

Basically, all this function does is that it [renders your component](https://github.com/testing-library/react-testing-library/blob/d0149e8ae498bdc02ee88d86546d76d6b7772ba1/src/pure.js#L52) using `ReactDOM.render` (or hydrate for server-side rendering) in a newly created `div` appended directly to `document.body`. You won't often need (at least in the beginning) the result from the `render` method, so I'll let you check the [docs](https://testing-library.com/docs/react-testing-library/api#render-result) as well.

#### Queries and `screen`

Once our component is rendered correctly, we can get the DOM elements using [screen](https://testing-library.com/docs/dom-testing-library/api-queries#screen) queries.

But what is `screen`? As said above, the component is rendered in `document.body`. Since it's common to query it, Testing Library exports an object with every query pre-bound to `document.body`. Note that we can also destructure [queries from the `render` result](https://testing-library.com/docs/react-testing-library/api#queries-1) but trust me, it's more convenient to use `screen`.

And now, you may think: _"what are these queries"_? They are utilities that allow you to query the DOM like a user would do it. Thus, you can find elements by label text, by a placeholder, by title.

Here are some queries examples taken from [the docs](https://testing-library.com/docs/dom-testing-library/api-queries#queries):

- `getByLabelText`: searches for the label that matches the given text passed as an argument and then finds the element associated with that label.
- `getByText`: search for all elements with a text node with textContent matching the given text passed as an argument.
- `getByTitle`: returns the element with a `title` attribute matching the given text passed as an argument.
- `getByPlaceholderText`: searches for all elements with a `placeholder` attribute and find one that matches the given text passed as an argument.

There are many variants to a particular query:

- `getBy`: returns the first matching node for a query, throws an error if no elements match, or finds more than one match.
- `getAllBy`: returns an array of all matching nodes for a query and throws an error if no elements match.
- `queryBy`: returns the first matching node for a query and returns null if no elements match. This is useful for asserting an element that is not present.
- `queryAllBy`: returns an array of all matching nodes for a query and returns an empty array (`[]`) if no elements match.
- `findBy`: return a **promise**, which resolves when an element is found which matches the given query.
- `findAllBy`: return a **promise**, which resolves to an array of elements when any elements are found which match the given query.

Using the right query at the right time can be challenging. I highly recommend that you check [Testing Playground](https://testing-playground.com/) to better know which queries to use in your apps.

Let's come back to our example:

```jsx
render(<Counter />);
const counter = screen.getByText("0");
const incrementButton = screen.getByText("+");
const decrementButton = screen.getByText("-");
```

In this example, we can see that we first render the `<Counter/>`. The base element of this component will look like the following:

```jsx
<body>
  <div>
    <Counter />
  </div>
</body>
```

Then, thanks to `screen.getByText`, we can query from `document.body` the increment button from, the decrement button and the counter. Hence, we will get for each button an instance of [HTMLButtonElement](https://developer.mozilla.org/fr/docs/Web/API/HTMLButtonElement) and for the counter an instance of [HTMLParagraphElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLParagraphElement).

### Act

Now that everything is set up, we can act. For that, we use `fireEvent` from [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/api-events):

```jsx
fireEvent((node: HTMLElement), (event: Event));
```

Simply put, this function takes a DOM node (that you can query with the queries seen above!) and fires DOM events such as `click`, `focus`, `change`, etc. You can dispatch many other events that you can find [by reading DOM Testing Library source code](https://github.com/testing-library/dom-testing-library/blob/dbbea6ee514399d0b37690ce5c56bb21f5ae2cb3/src/events.js#L1).

Our example is relatively simple as we just want to click a button, so we simply do:

```jsx
fireEvent.click(incrementButton);
// OR
fireEvent.click(decrementButton);
```

### Assert

Here comes the last part. Firing an event usually triggers some changes in your app. So we must do some assertions to make sure these changes happened. In our test, a good way to do so is to make sure the count rendered to the user has changed. Thus, we just have to assert the [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) property of `counter` is incremented or decrement:

```jsx
expect(counter.textContent).toEqual("1");
expect(counter.textContent).toEqual("0");
```

And tadaaa! We successfully wrote a test that doesn't test implementation details. 🥳

## Test a to-do app

Let's go deeper into this part by testing a more complex example. The app we're going to test is a simple to-do app whose features are the following:

- Add a new to-do
- Mark a to-do as completed or active
- Remove a to-do
- Filter the to-dos: all, active, and done to-dos

Yes, I know, you may be sick of to-do apps in every tutorial, but hey, they're great examples!

Here is the code:

```jsx
// Todos.js
import React from "react"

function Todos({ todos: originalTodos }) {
  const filters = ["all", "active", "done"]
  const [input, setInput] = React.useState("")
  const [todos, setTodos] = React.useState(originalTodos || [])
  const [activeFilter, setActiveFilter] = React.useState(filters[0])

  const addTodo = (e) => {
    if (e.key === "Enter" && input.length > 0) {
      setTodos((todos) => [{ name: input, done: false }, ...todos])
      setInput("")
    }
  }

  const filteredTodos = React.useMemo(
    () =>
      todos.filter((todo) => {
        if (activeFilter === "all") {
          return todo
        }

        if (activeFilter === "active") {
          return !todo.done
        }

        return to-do.done
      }),
    [todos, activeFilter]
  )

  const toggle = (index) => {
    setTodos((todos) =>
      todos.map((todo, i) =>
        index === i ? { ...todo, done: !todo.done } : todo
      )
    )
  }

  const remove = (index) => {
    setTodos((todos) => todos.filter((todo, i) => i !== index))
  }

  return (
    <div>
      <h2 className="title">To-dos</h2>
      <input
        className="input"
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={addTodo}
        value={input}
        placeholder="Add something..."
      />
      <ul className="list-todo">
        {filteredTodos.length > 0 ? (
          filteredTodos.map(({ name, done }, i) => (
            <li key={`${name}-${i}`} className="todo-item">
              <input
                type="checkbox"
                checked={done}
                onChange={() => toggle(i)}
                id={`todo-${i}`}
              />
              <div className="todo-infos">
                <label
                  htmlFor={`todo-${i}`}
                  className={`todo-name ${done ? "todo-name-done" : ""}`}
                >
                  {name}
                </label>
                <button className="todo-delete" onClick={() => remove(i)}>
                  Remove
                </button>
              </div>
            </li>
          ))
        ) : (
          <p className="no-results">No to-dos!</p>
        )}
      </ul>
      <ul className="list-filters">
        {filters.map((filter) => (
          <li
            key={filter}
            className={`filter ${
              activeFilter === filter ? "filter-active" : ""
            }`}
            onClick={() => setActiveFilter(filter)}
          >
            {filter}
          </li>
        ))}
      </ul>
    </div>
  )
}

export default Todos
```

### More on fireEvent

We saw previously how `fireEvent` allows us to click on a button queried with RTL queries (such as `getByText`). Let's see how to use other events.

In this app, we can add a new to-do by writing something in the input and pressing the `Enter` key. We'll need to dispatch two events:

- `change` to add a text in the input
- `keyDown` to press the enter key.

Let's write the first part of the test:

```jsx
test("adds a new to-do", () => {
  render(<Todos />);
  const input = screen.getByPlaceholderText(/add something/i);
  const todo = "Read Master React Testing";

  screen.getByText("No to-dos!");

  fireEvent.change(input, { target: { value: todo } });
  fireEvent.keyDown(input, { key: "Enter" });
});
```

In this code, we:

1. Query the input by its placeholder.
2. Declare the to-do we're going to add
3. Assert no to-dos were using `getByText` (if `No to-dos!` was not in the app, `getByText` would throw an error)
4. Add the to-do in the input
5. Press the enter key.

One thing that may surprise you is the second argument we pass to `fireEvent`. Maybe you would expect it to be a single string instead of an object with a `target` property.

Well, under the hood, `fireEvent` dispatches an event to mimic what happens in a real app (it makes use of the [dispatchEvent](https://developer.mozilla.org/fr/docs/Web/API/EventTarget/dispatchEvent) method). Thus, we need to dispatch the event as it would happen in our app, including setting the `target` property. The same logic goes for the `keyDown` event and the `key` property.

What should happen if we add a new to-do?

- There should be a new item in the list
- The input should be empty

Hence, we need to query somehow the new item in the DOM and make sure the `value` property of the input is empty:

```jsx
screen.getByText(todo);
expect(input.value).toBe("");
```

The full test becomes:

```jsx
test("adds a new to-do", () => {
  render(<Todos />);
  const input = screen.getByPlaceholderText(/add something/i);
  const todo = "Read Master React Testing";

  screen.getByText("No to-dos!");

  fireEvent.change(input, { target: { value: todo } });
  fireEvent.keyDown(input, { key: "Enter" });

  screen.getByText(todo);
  expect(input.value).toBe("");
});
```

### Better assertions with jest-dom

The more you'll write tests with RTL, the more you'll have to write assertions for your different DOM nodes. Writing such assertions can sometimes be repetitive and a bit hard to read. For that, you can install another Testing Library tool called [`jest-dom`](https://github.com/testing-library/jest-dom).

`jest-dom` provides a set of custom jest matchers that you can use to extend jest. These will make your tests more declarative, clear to read and to maintain.

There are many matchers you can use such as:

- [toBeInTheDocument](https://github.com/testing-library/jest-dom#tobeinthedocument)
- [toBeDisabled](https://github.com/testing-library/jest-dom#tobedisabled)
- [toHaveTextContent](https://github.com/testing-library/jest-dom#tohavetextcontent)
- [toHaveValue](https://github.com/testing-library/jest-dom#tohavevalue)
- And more!

You can install it with the following command:

```shell
npm install --save-dev @testing-library/jest-dom
```

Then, you have to import the package **once** to extend the Jest matchers:

```js:setupTest.js
import "@testing-library/jest-dom/extend-expect"
```

**Note**: I recommend that you do that in `src/setupTests.js` if you use [Create React App](https://create-react-app.dev/docs/running-tests#src-setuptestsjs-1). If you don't use CRA, import it in one of the files defined in the [`setupFilesAfterEnv`](https://jestjs.io/docs/en/configuration#setupfilesafterenv-array) key of your Jest config.

Let's come back to our test. By installing `jest-dom`, your assertion would become:

```jsx
expect(input).toHaveValue("");
```

It's not much, but it's more readable, convenient and it improves the developer experience! 🙌

**💡 If you want to see more test examples on this to-do app, I created a repo that contains [all the examples of this article](https://github.com/thomaslombart/react-testing-library-examples)!**

## Asynchronous tests

I agree the counter and the to-do app are contrived examples. In fact, most real-world applications involve asynchronous actions: data fetching, lazy-loaded components, etc. Thus, you need to handle them in your tests.

Luckily for us, RTL gives us asynchronous utilities such as [`waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor) or [`waitForElementToBeRemoved`](https://testing-library.com/docs/dom-testing-library/api-async#waitforelementtoberemoved).

In this part, we will use a straightforward posts app whose features are the following:

- Create a post
- See the newly created post in a list of posts
- See an error if something has gone wrong while creating the post.

Here is the code:

```jsx
// Posts.js
import React from "react"

import { addPost } from "./api"

function Posts() {
  const [posts, addLocalPost] = React.useReducer((s, a) => [...s, a], [])
  const [formData, setFormData] = React.useReducer((s, a) => ({ ...s, ...a }), {
    title: "",
    content: "",
  })
  const [isPosting, setIsPosting] = React.useState(false)
  const [error, setError] = React.useState("")

  const post = async (e) => {
    e.preventDefault()

    setError("")

    if (!formData.title || !formData.content) {
      return setError("Title and content are required.")
    }

    try {
      setIsPosting(true)
      const {
        status,
        data: { id, ...rest },
      } = await addPost(formData)
      if (status === 200) {
        addLocalPost({ id, ...rest })
      }
      setIsPosting(false)
    } catch (error) {
      setError(error.data)
      setIsPosting(false)
    }
  }

  return (
    <div>
      <form className="form" onSubmit={post}>
        <h2>Say something</h2>
        {error && <p className="error">{error}</p>}
        <input
          type="text"
          placeholder="Your title"
          onChange={(e) => setFormData({ title: e.target.value })}
        />
        <textarea
          type="text"
          placeholder="Your post"
          onChange={(e) => setFormData({ content: e.target.value })}
          rows={5}
        />
        <button className="btn" type="submit" disabled={isPosting}>
          Post{isPosting ? "ing..." : ""}
        </button>
      </form>
      <div>
        {posts.map((post) => (
          <div className="post" key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

export default Posts
```

```javascript
// api.js
let nextId = 0

export const addPost = (post) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.1) {
        resolve({ status: 200, data: { ...post, id: nextId++ } })
      } else {
        reject({
          status: 500,
          data: "Something wrong happened. Please, retry.",
        })
      }
    }, 500)
  })
}
```

Let's test the post creation feature. To do so, we need to:

1. Mock the API to make sure a post creation doesn't fail
2. Fill in the tile
3. Fill in the content of the post
4. Click the Post button

Let's first query the corresponding elements:

```jsx
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

import { addPost as addPostMock } from "./api";
import Posts from "./Posts";

jest.mock("./api");

describe("Posts", () => {
  test("adds a post", async () => {
    addPostMock.mockImplementation((post) =>
      Promise.resolve({ status: 200, data: { ...post, id: 1 } })
    );
    render(<Posts />);
    const title = screen.getByPlaceholderText(/title/i);
    const content = screen.getByPlaceholderText(/post/i);
    const button = screen.getByText(/post/i);
    const postTitle = "This is a post";
    const postContent = "This is the content of my post";
  });
});
```

You can see I've used queries differently this time. Indeed, when you pass a string to a `getBy` query, it expects to match **exactly** that string. If there's something wrong with one character, then the query fails.

However, the queries also accept a **regular expression as an argument.** It can be handy if you want to quickly query a long text or if you want to query a substring of your sentence in case you're still not sure of the wording.

For example, I know the placeholder of my content should include the word _"post"_. But, maybe the placeholder will see its wording change at some point and I don't want my tests to break because of this simple change. So I use:

```jsx
const content = screen.getByPlaceholderText(/post/i);
```

**Note**: for the same reason, I use `i` to make the search case-insensitive. That way, my test doesn't fail if the case changes. Caution though! If the wording is important and shouldn't change, don't make use of regular expressions.

Then, we have to fire the corresponding events and make sure the post has been added. Let's try it out:

```jsx
test("adds a post", () => {
  addPostMock.mockImplementation((post) =>
    Promise.resolve({ status: 200, data: { ...post, id: 1 } })
  );
  render(<Posts />);
  const title = screen.getByPlaceholderText(/title/i);
  const content = screen.getByPlaceholderText(/post/i);
  const button = screen.getByText(/post/i);
  const postTitle = "This is a post";
  const postContent = "This is the content of my post";

  fireEvent.change(title, { target: { value: postTitle } });
  fireEvent.change(content, { target: { value: postContent } });
  fireEvent.click(button);

  // Oops, this will fail ❌
  expect(screen.queryByText(postTitle)).toBeInTheDocument();
  expect(screen.queryByText(postContent)).toBeInTheDocument();
});
```

If we had run this test, it wouldn't work! In fact, RTL can't query our post title. But why? To answer that question, I'll have to introduce you to one of your next best friends: `debug`.

### Debugging tests

Simply put, `debug` is a utility function attached to the `screen` object that prints out a representation of your component's associated DOM. Let's use it:

```jsx
test("adds a post", () => {
  // ...

  fireEvent.change(title, { target: { value: postTitle } });
  fireEvent.change(content, { target: { value: postContent } });
  fireEvent.click(button);

  debug();

  expect(screen.queryByText(postTitle)).toBeInTheDocument();
  expect(screen.queryByText(postContent)).toBeInTheDocument();
});
```

In our case, `debug` outputs something similar to this:

```html
<body>
  <div>
    <div>
      <form class="form">
        <h2>Say something</h2>
        <input placeholder="Your title" type="text" />
        <textarea placeholder="Your post" rows="5" type="text" />
        <button class="btn" disabled="" type="submit">Post ing...</button>
      </form>
      <div />
    </div>
  </div>
</body>
```

Now that we know what your DOM looks like, we can guess what's happening. The post hasn't been added. If we closely pay attention, we can see the button's text is now `Posting` instead of `Post`.

Do you know why? Because posting a post is **asynchronous** and we're trying to execute the tests without waiting for the asynchronous actions. We're just in the **Loading** phase. We can only make sure some stuff is going on:

```jsx
test("adds a post", () => {
  // ...

  fireEvent.change(title, { target: { value: postTitle } });
  fireEvent.change(content, { target: { value: postContent } });
  fireEvent.click(button);

  expect(button).toHaveTextContent("Posting");
  expect(button).toBeDisabled();
});
```

### Wait for changes

We can do something about that. More precisely, RTL can do something about that with [asynchronous utilities](https://testing-library.com/docs/dom-testing-library/api-async) such as `waitFor`:

```ts
function waitFor<T>(
  callback: () => void,
  options?: {
    container?: HTMLElement;
    timeout?: number;
    interval?: number;
    onTimeout?: (error: Error) => Error;
    mutationObserverOptions?: MutationObserverInit;
  }
): Promise<T>;
```

Simply put, `waitFor` takes a callback that contains expectations and waits for a specific time until these expectations pass.

By default, this time is at most `1000ms` at an interval of `50ms` (the first function call is fired immediately). This callback is also run every time a child is added or removed in your component's `container` using [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).

We're going to make use of that function and put our initial assertions in it. The test now becomes:

```jsx
import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";

// ...

describe("Posts", () => {
  test("adds a post", async () => {
    // ...

    expect(button).toHaveTextContent("Posting");
    expect(button).toBeDisabled();

    await waitFor(() => {
      screen.getByText(postTitle);
      screen.getByText(postContent);
    });
  });
});
```

If you're using CRA, maybe you encountered the following error:

```
TypeError: MutationObserver is not a constructor
```

That's normal. [DOM Testing Library v7](https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0) removed a shim of `MutationObserver` as it's now widely supported. However, at the time of writing, CRA still uses an older version of Jest (24 or before), which uses a JSDOM environment where `MutationObserver` doesn't exist.

Two steps to fix it. First, install `jest-environment-jsdom-sixteen` as a dev dependency. Then, update your `test` script in your `package.json` file:

```
"scripts": {
  ...
  "test": "react-scripts test --env=jest-environment-jsdom-sixteen"
  ...
}
```

Now, it passes! 🎉

There is also another way of testing asynchronous things with `findBy*` [queries](https://github.com/testing-library/dom-testing-library/blob/19901e13b6acd11df580ddf6dd35540a39c5eaaf/src/query-helpers.js#L165) which is just a combination of `getBy*` queries and `waitFor`:

```jsx
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

// ...

describe("Posts", () => {
  test("adds a post", async () => {
    // ...

    expect(button).toHaveTextContent("Posting");
    expect(button).toBeDisabled();

    await screen.findByText(postTitle);
    screen.getByText(postContent);
  });
});
```

**Note:** In the past, you could also use [`wait`](https://testing-library.com/docs/dom-testing-library/api-async#wait-deprecated-use-waitfor-instead) and [`waitForElement`](https://testing-library.com/docs/dom-testing-library/api-async#waitforelement-deprecated-use-find-queries-or-waitfor) but they're deprecated now. Don't worry if you find them in certain tests!

We know for sure that the API successfully returned the full post after the `await` statement, so we don't have to put async stuff after.

And remember, `findByText` is asynchronous! If you forget the `await` statement a little bit too much, I encourage you to install the following plugin: [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library). It contains [a rule](https://github.com/testing-library/eslint-plugin-testing-library/blob/master/docs/rules/await-async-query.md) that prevent you from doing so! 😉

---

Pheeeew! That part was not easy.

Hopefully, these three examples allowed you to have an in-depth look at how you can start to write tests for your React apps, but that's just the tip of the iceberg! A complex app often uses `react-router`, `redux`, React's Context, third-party libraries (`react-select` for example). Kent C. Dodds has a complete course on that (and much more) called [Testing JavaScript](https://testingjavascript.com/) that I really recommend!