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!
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
Clone the repo
Run:
yarn startYou should see something like this:
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 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 4.134sRan 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:
- Load the Login screen
- Click on the username input, and type the username
- Click on the password input, and type the password
- Click the Submit button
- 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 ourButton
component - We've marked the test
async
to enable us to usefindByText()
- 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 use
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.