Learn Integration Testing with React Hook Form

Do you sometimes worry that your tests don't make sense? Struggling to get what people mean by "test from the user's perspective" and the classic piece of advice "test functionality, not implementation details"?

You're not alone!

I felt the same thing when I started using React Testing Library. I used to think of testing as just checks you had to do on individual components, directly asserting against props and state, and would struggle to think about how to test the integration of several components together.

What does "integration test" even mean?

It helps to think of integration testing like a bigger unit test, except the unit you're testing is the combination of several smaller components.

More concretely, instead of just testing a Button component, or a TextField component in isolation, we're going to test that they work when placed together into a form.

Let's get started!

We're going to be testing a form almost every public web app you're going to build has: a Login form. It's probably one of the most important parts of your app (in terms of business value), so let's be confident it actually works!

React Login Form

Setup

We're going to be using create-react-app, because it comes bundled with @testing-library/react. I'm also using react-hook-form to build our form, because it's the fastest way I know to build a form in React apps.

Steps

  1. Clone the repo

  2. Run:

    yarn start

    You should see something like this: React Login Form after starting

  3. At this point, if you ran yarn test, you would see the following:

PASS src/pages/Login.test.js
✓ integration test (177ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.134s
Ran all test suites.
Watch Usage: Press w to show more.

So how do we get here?

First off, here's the code from our integration test:

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import Login from './Login';
test('integration test', async () => {
const USER = 'some-username';
const PASS = 'some-pass';
render(<Login />);
const userInput = screen.getByLabelText(/username/i);
user.type(userInput, USER);
const passwordInput = screen.getByLabelText(/password/i);
user.type(passwordInput, PASS);
const submitButton = screen.getByText(/submit/i);
fireEvent.click(submitButton);
expect(await screen.findByText(/your username/i)).toBeInTheDocument();
expect(await screen.findByText(/your password/i)).toBeInTheDocument();
});

It might look like we're doing a few complicated things above, but essentially, we've taken the steps a user takes when logging in, and turned it into a test:

  1. Load the Login screen
  2. Click on the username input, and type the username
  3. Click on the password input, and type the password
  4. Click the Submit button
  5. Wait around for some sign that the Login worked

Some testing-library specific things worth calling out:

  • We import '@testing-library/user-event' to let us to type into our inputs
  • We import fireEvent from the '@testing-library/react' library to let us to click on our Button component
  • We've marked the test async to enable us to use findByText()
    • We use findByTest() after performing an action that may be asynchronous - like filling out a form. (findByText returns a Promise, letting us await until it finds the text it's looking for before continuing)

We've built a test that can type into our TextField components, click on our Button component, and trigger the Form component's onSubmit function, without ever referring to implementation details!

If you're confused about findByText vs getByText, don't worry - that's normal. In general though, findBy functions are for use after async actions (like clicking the Submit button), and getBy functions are for general use.

React Testing Library also has a cheatsheet with tips to help you decide which one to use.

Conclusion

You've just started to understand integration testing, but you best believe there's a lot more to it than this article!

If you want a more advanced perspective of integration testing your forms, I highly recommend reading the testing section of React Hook Form's Advanced Usage guide.

(Shameless plug for the useEffect book I wrote below)

Tired of infinite re-renders when using useEffect?

A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.

Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.

In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

Master useEffect, in a single afternoon.

useEffect By Example's book cover