Integration tests provide a good balance between test cost and value. Using react-testing-library to write integration tests for React applications, either in place of or alongside component unit tests, can enhance code maintainability without sacrificing development speed.
If you’d like a sneak peek before moving forward, you can check out an example demonstrating how to use react-testing-library for React app integration tests here.
Why Invest in Integration Testing?
“Integration tests find a good balance between confidence and speed/cost trade-offs. Therefore, it’s recommended to focus most (but not all) of your testing efforts in this area.” – Kent C. Dodds in Write tests. Not too many. Mostly integration.
Unit testing React components is a standard practice, often employing popular libraries like “enzymes” for React testing, specifically its “shallow” method. This allows testing components separately from the rest of the application. However, since React applications rely heavily on component composition, unit tests alone cannot guarantee a bug-free application.
For instance, modifying a component’s accepted props and updating its corresponding unit tests might show all tests passing, but the application could still break if another component wasn’t updated accordingly.
Integration tests can offer peace of mind when making changes to a React application by verifying that the component composition results in the desired user experience.
What We Want and What We Don’t Want in React App Integration Tests
Here’s what React developers aim to achieve when writing integration tests:
- Test application use-cases from the end-user’s point of view, simulating how users interact with controls and access information on a web page.
- Mock API calls to avoid dependency on API availability and state for test results.
- Mock browser APIs (like local storage) as they don’t exist in the testing environment.
- Make assertions about the React DOM state (whether it’s a browser DOM or a native mobile environment).
Conversely, here’s what we should try to avoid:
- Testing implementation specifics. Implementation changes should only break a test if they introduce a genuine bug.
- Excessive mocking. The goal is to test how different app parts work together.
- Shallow rendering. We want to test the entire component composition, down to the smallest unit.
Why Choose React-testing-library?
These requirements make react-testing-library an excellent choice, as its core principle is to enable testing React components in a way that mirrors how actual users interact with them.
This library, along with its optional companion tool, companion libraries, allows writing tests that interact with and assert on the state of the DOM.
Setting Up a Sample App
Our sample app for integration tests will implement a basic scenario:
- The user provides a GitHub username.
- The app displays a list of public repositories associated with that username.
The implementation details shouldn’t matter for integration testing. However, to stay realistic, the app will follow common React patterns:
- Single-page app (SPA) structure
- API requests
- Global state management
- Internationalization support
- Use of a React component library
The app’s source code is available here.
Writing Integration Tests
Installing Dependencies
Using yarn:
| |
Or with npm:
| |
Creating a Test Suite File
We’ll create viewGitHubRepositoriesByUsername.spec.js within our application’s ./test folder. Jest will automatically detect it.
Importing Dependencies
| |
Setting up the Test Suite
| |
Notes:
- Before each test, mock the GitHub API to return a list of repositories for a specific username.
- Clean the test React DOM after each test to ensure a fresh start for subsequent tests.
describeblocks outline the integration test use case and its flow variations.- Tested flow variations include:
- Valid username with associated public repositories.
- Valid username with no associated public repositories.
- Non-existent GitHub username.
itblocks use async callbacks to handle asynchronous operations within the tested use cases.
Writing the First Flow Test
The first step is to render the app:
| |
The render method, imported from @testing-library/react, renders the app within the test React DOM and returns DOM queries bound to the app container. These queries help locate and interact with DOM elements and assert their state.
Next, we simulate the user entering a username into the provided field:
| |
The userEvent helper, imported from @testing-library/user-event, provides the type method to mimic a user typing into a text field. It takes the target DOM element and the input string as arguments.
Users usually find elements through associated text, like labels or placeholders. We’ll use the getByPlaceholderText query returned by render to locate the username field.
It’s good practice to configure the localization module to return localization keys instead of actual values during tests. For instance, instead of “Enter GitHub username” for userSelection.usernamePlaceholder, return “userSelection.usernamePlaceholder”. This avoids relying on text that might change.
The text field should update as the user types:
| |
Next, we simulate the user clicking the submit button and expect to see the repository list:
| |
userEvent.click simulates a click event, and getByText finds the button by its text. The closest modifier helps select the correct element type.
Note: In integration tests, steps often have both action and assertion roles. Here, clicking the button asserts its clickability.
We’ve asserted that the repository list section is visible. Now, let’s ensure the user sees a loading indicator while fetching repositories from GitHub and that the app doesn’t prematurely indicate no repositories found:
| |
Note the use of getBy to assert an element’s presence and queryBy for the opposite. queryBy doesn’t throw an error if the element is missing.
We then ensure the app eventually fetches and displays the repositories:
| |
The waitForElement method waits for a DOM update that fulfills the provided assertion. Here, we wait for the app to display the name and description of each repository returned by the mocked GitHub API.
Finally, the loading indicator and any error messages should disappear:
| |
Our complete React integration test now looks like this:
| |
Testing Alternate Flows
Test the scenario where the entered username has no associated public repositories:
| |
Test the scenario where the entered GitHub username doesn’t exist:
| |
Why React Integration Tests are Great
Integration testing hits a sweet spot for React apps. They effectively catch bugs and facilitate test-driven development (TDD) while remaining resilient to implementation changes.
React-testing-library, demonstrated here, is an excellent tool for writing React integration tests, allowing you to interact with the app from the user’s perspective and validate its state and behavior accordingly.
These examples can be a starting point for incorporating integration tests into new or existing React projects. The complete code, including the app implementation, is available at GitHub.