Jeff Auriemma Software maker, coffee taker

A low-friction way to do TDD with React

4 min read 1254

The React logo.

Write the test first.

  • Beyoncé (paraphrased)

If you’re like Beyoncé, you know that writing your UI component code should only be done once you have a solid set of tests.

Ideally. You know, if it works out that way. You try your best.

The thing is, test-driven development (TDD) orthodoxy suggests that you probably shouldn’t write tests that depend on a specific implementation strategy. But for React developers, popular tools such as Enzyme and React Test Renderer make it so easy to write tests that are tightly coupled with the details of the source code.

Don’t get me wrong: those are great tools, but their APIs make it close to impossible to write a unit test that won’t break if a component name or DOM selector changes.

What makes a good test?

You won’t find any shortage of thinkpieces or books about good unit testing practices. If you want to write a good test, they say, target the public interface. So if it’s a class, test the public methods of that class, not the private methods. This conventional wisdom serves us well when testing traditional object-oriented programs.

React apps, though, are not traditional object-oriented programs. Their public interfaces should not be thought of as messages between objects. Instead, consider the public interface of your React component as the parts that the user can see or interact with.

That is ultimately the point of a UI component library: it manages messages between the user and the application. So if you want to write a good unit test for your React code, target the public interface — the parts that the user can perceive and interact with.

What text can the user see? What changes when they interact with an element?

These are the questions a component unit test should answer. And as it happens, these types of questions are very similar to the types of requirements that product stakeholders tend to write for user interfaces.

Consider the following specification for a simple weather app:

We made a custom demo for .
No really. Click here to check it out.

  • The page should have a button with the text “Get weather”
  • When the user taps the button, the current weather in New York should appear

Like a good TDD’er, you set out to write a test for each of these criteria. Let’s look at one possible approach using the popular Enzyme library:

import { mount } from "enzyme";

describe("App", () => {
  it("should render a button", () => {
    const element = mount(<App />);
    const button = element.find("button");
    expect(button).toHaveLength(1);
  });

  it("should fetch a weather forecast upon tapping the button", async () => {
    const element = mount(<App />);
    const button = element.find("button");
    button.simulate("click");
    await wait(2000); // homespun utility function that waits X milliseconds before resolving
    expect(element.text()).toContain("New York");
    expect(element.text()).toContain("°");
  });
});

This is a perfectly serviceable test, but it’s brittle. This test depends on a <button> element being interactive.

We all know that when product requirements say “button,” they mean “thing that looks and acts like a button.”

So what if, during implementation, we decide it’s best to use <div tabindex="0"role="button"> or <a href="/forecast-for-new-york">?

What if the request takes longer than two seconds? We would, of course, need to update the test. That’s not the end of the world, but it’s also not the way TDD is ideally done.

This feature is so simple that these edge cases are definitely in the “nitpick” category, but it’s not hard to imagine where these distinctions become more consequential. The button selector is a “private” interface, so the user doesn’t perceive nor care what HTML tag is being used to express an element.

They just want to find “Get weather” and get the dang weather! And they won’t wait two seconds if it only takes 100 milliseconds, either.

Remember: a component’s true public interface is that which is able to be perceived and be interacted with by the user, not abstractions such as selectors or component names.

So if Enzyme and similar tools such as React Test Renderer won’t do the trick, what will?

React Testing Library

React Testing Library, of course! Initially released in early 2018, this relative newcomer is picking up lots of steam in the React community, and for good reason.

React Testing Library’s API offers a refreshing, user-centric alternative to Enzyme, et al. Let’s write the same two tests as before, this time using React Testing Library (RTL):

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

describe("App", () => {
  it("should render a button", () => {
    render(<App />);
    const button = screen.queryByText("Get weather");
    expect(button).not.toBeNull();
  });

  it("should fetch a weather forecast upon tapping the button", async () => {
    render(<App />);
    const button = screen.queryByText("Get weather");
    fireEvent.click(button);
    await screen.findByText("New York", { exact: false });
    expect(screen.queryByText("°", { exact: false })).not.toBeNull();
  });
});

RTL’s API is straightforward. We find elements by label, text, aria-label, or other user-facing attributes. Selectors are completely sidelined.

Out with find("DestinysChildImg") and in with findByAltText("Destiny's Child performing at their reunion tour in 2022").

I want to also call attention to one of the several tools RTL gives us to handle asynchronous functionality:

await screen.findByText("New York", { exact: false });

This does exactly what you think it does.

findByText queries the DOM on an interval until it (a) times out, or (b) finds the element. In real life, response times for asynchronous services will vary a great deal depending on that service’s performance and the power and bandwidth of the client device.

Yet, JavaScript unit tests often go to great lengths to tame asynchronous effects via mocking or best-guess wait functions. RTL’s async tools solve the problem of expressing asynchronous effects in a test suite. In other words, it waits for the page to change just like your users would.

This is as close to plain English as a JavaScript test suite can get in 2020. It’s so intuitive to translate product requirements into unit tests using RTL, which makes it an ideal choice for TDD practitioners.

So, we’ve written our tests. Let’s close the loop and write some source code!

First, we’ll make the first test pass:

import React from "react";

const App = () => {
  return (
    <div>
      Get weather
    </div>
  );
};

export default App;

Then, we’ll make the second test pass by introducing the interactive elements:

import React, { useState } from "react";

const App = () => {
  const [weather, updateWeather] = useState(null);

  const fetchAndUpdateWeather = async () => {
    const response = await fetch("https://wttr.in/New York?format=3");
    const body = await response.text();
    updateWeather(body);
  };

  return (
    <div>
      <div>{weather}</div>
      <div>
        <button onClick={fetchAndUpdateWeather}>Get weather</button>
      </div>
    </div>
  );
};
export default App;

I don’t care how small an app is, I’ll never get tired of seeing the tests turn green the first time. Total bliss.

I’ve aggregated the tests and source code into a CodeSandbox for your reference:

Conclusion

In React Testing Library, you get a tool that enables true test-driven development: the API all but forces you to query by items that are perceptible to end users, and asynchronous effects are handled elegantly while approximating real-world user behavior.

You can finally write your React unit tests first without a care in the world for how the source code will look.

“Irreplaceable”

  • Beyoncé, but really this time

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Jeff Auriemma Software maker, coffee taker

Leave a Reply