Integration Tests, Unit Tests and React Testing Library

The time has come! You updated the README documentation to reflect every prop change also described so declaratively in your TypeScript code. You covered every "action", every "reducer", every "saga" and every component with unit tests to meet the code coverage percent defined by your team. You resolved all thought provoking discussion in your pull request. It's been days, weeks, and now the time has come to click the "merge" button. But, you keep asking yourself - "did I miss something"? You may have indeed neglected something - the user.

In the twists and turns of software engineering, it can be difficult to determine an effective testing strategy. Whether you're dealing with React, TypeScript, Redux, GraphQL, all of the above or another front end stack, chances are you're dealing with frameworks on top of frameworks. Common ways of testing a React UI typically involve examining internal state and props from component output with the help of a library like Enzyme. By using Enzyme, we're required to use an "adapter" for the specific version of React. The more complex the stack the more complex the testing effort with this approach, because we're focusing on implementation.

React Testing Library and Integration Testing

The more your tests resemble the way your software is used,
the more confidence they can give you.

React Testing Library proposes a mindset change.

The primary purpose of the React Testing Library is to give you confidence by testing your components in the way the user will use them. Users don't care what happens behind the scenes. They just see and interact with the output. So, instead of accessing the components' internal API, or evaluating the state, you'll get more confidence by writing your tests based on the component output.

Kent Dodds, the founding contributor of React Testing Library, wrote an interesting blog post called "Write tests. Not too many. Mostly integration." in which he cites a tweet from Guillermo Rauch‏ and remarkably articulates each of those three statements.

I couldn't agree more with these statements. With years of first hand experience wrestling Enzyme (at times), spending hours (to days) mocking implementations, and many times mindlessly updating tests to fulfill a coverage requirement, I too have taken a step back to ask: "all this - for what?!" I arrived at a conclusion mirroring the mindset of React Testing Library. User experience should be the focus of front end testing.

Integration testing is a level of software testing in which individual units are combined and tested as a group. The purpose of this level of testing is to expose faults in the interaction between integrated units. Integration testing makes a lot of sense when you think about the nature of UI testing.

Browser APIs alone add enough variability to justify integration testing a UI. Both React Testing Library and Enzyme emulate the browser with jsdom, a JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js.

A Mindset Change by Example with React Testing Library

Based on the React Testing Library Guiding Principles, tests should resemble how users interact with your code (component, page, etc.) as much as possible. Below is a simple example from my tiny React chart component library project (Storybook demo here). The key detail is that I'm testing a prop named valuePrefix to confirm a tooltip presents correct content to the user (a prepended $ sign or nothing prepended when valuePrefix is omitted). I'm testing what the user will see on the screen versus the implementation details or output of my component. In this test I don't care about the resulting props or internal state, because those details are not user facing.

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// ... more imports and other code here (excluded for this example)

describe('TimeSeriesChart', () => {
  describe('tooltip', () => {
    test('displays value', () => {
      render(<TimeSeriesChart data={mockData} height={100} />);

      userEvent.hover(screen.getByTestId('time-series-chart-bar'));
      expect(screen.getByText(/1,000|1,500|2,000/i)).toBeInTheDocument();
    });

    test('displays value with prefix', () => {
      render(<TimeSeriesChart data={mockData} height={100} valuePrefix="$" />);

      userEvent.hover(screen.getByTestId('time-series-chart-bar'));
      expect(screen.getByText(/\$1,000|\$1,500|\$2,000/i)).toBeInTheDocument();
    });
  });
});

Front End Testing Guidelines

With a mindset change to focus more on the user, below is a summary of points to establish an effective front end testing strategy.

  • Empathize with the user as you develop. Gather expectations of features in advance of building them and ensure they align with real users. Try to step back from different stages of development to ask yourself - what are all the paths a user may take and how can each be tested.
  • Don't quantify your testing strategy with coverage numbers. It's rewarding to see a visualization of success and easy to point fingers when numbers are low, but by doing this we only provide a means of accomplishing a false sense of security. Focus more on what is being tested and how in terms of the user. Focus less on achieving a higher volume of less relevant tests to fulfill your code coverage requirement.
  • Integration test components and groups of components. Unit test React Hooks. Building your own Hooks lets you extract component logic into reusable functions. We can write business logic in hooks and unit test independently without interference of browser APIs. Without the DOM, we can independently unit test with Jest alone. We can reserve React Testing Library for integration testing of components and groups of components.