Kermode

/ˈkɜːrmoʊd/
The Spirit Bear

Written by Gray Gilmore

Kermode

I make websites for a living. I love CSS and building simple and modular stylesheets. Somewhere along the way I also turned into a Ruby developer. Life comes at you fast I guess. You can read my resume to learn about my journey so far.

Other Projects

Say Hello

You can find me as @graygilmore on several platforms:

Good ol' fashion email works, too: hello@kermode.co

You can subscribe to this blog via RSS: XML, JSON

Writing user-centric tests for your React components

Prerequisites

This post is for folks that are already working with React components and a testing framework like Jest. If you’re just getting started on JavaScript testing check out some of the resources at the bottom.

Early in my career there were two things that I found incredibly difficult: naming things and writing tests. It is still impossible to name things but I have found a great joy out of writing tests!

When my career transitioned from frontend-only to Rails I struggled with some of the concepts of minitest and RSpec. It was foreign to me and at times slowed me down causing me to question their value entirely. No longer! Writing tests is one of my favourite parts of software development now and an important part of my workflow!

For whatever reason I had a really hard time transferring my love of testing in Rails applications to the work I was doing with React. Something about JavaScript testing just didn’t feel right to me and I struggled developing a mental model of what a “good test” looked like. All of that changed when a project I’ve been working on started transitioning from Enzyme to React Testing Library.

React Testing Library

Things started to click for me when I bought into Testing Library’s guiding principle:

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

In other words: focus on how your users are experiencing your application and not on how you’ve implemented it. This might feel a little bit different to unit testing classes in Ruby but at the core they are very similar: define a piece of functionality and test its output. That can feel simpler in Ruby because you can define a method and test its singular outcome whereas testing for markup can feel a little messier. Thinking about testing from the user’s perspective allowed me to narrow the things I was testing for while also making sure my coverage was solid.

We were previously using Enzyme where I had a hard time reasoning with the tests that we were writing:

const wrapper = mount(<h1 className="foo">Howdy</h1>);
expect(wrapper.find('.foo').exists()).toEqual(true);

Is the presence of a class named foo really the thing I want to be asserting on here? The user of the site doesn’t care about the naming of an attribute. You can still test classes and other attributes with React Testing Library but that’s not what makes the library great to use.

React Testing Library encourages you to go in the other direction and write tests from the user’s perspective instead:

const { getByText } = render(<h1 className="foo">Howdy</h1>);
expect(getByText('Howdy')).toBeDefined();

Our assertion is much clearer! We don’t really care what the class name of the element is. What we care about is that our users see a heading of “Howdy” so let’s explicitly test for that! You can test for the text of the element with Enzyme but React Testing Library encourages you to make that the only way you test.

Dependencies

Before we dig into our example component and its tests we should talk briefly about some of the things that we need in order to get this going.

Jest

The React Testing Library in and of itself is not a test runner. We need Jest to provide us with the actual framework that is going to run our tests and tell us if things have failed or succeeded.

@testing-library/jest-dom

jest-dom provides an additional set of DOM element matchers for Jest. This is a nice-to-have addition but I really like how it makes some of the tests read:

// test that our Howdy heading exists
expect(getByText('Howdy')).toBeDefined();

// with @testing-library/jest-dom we can be more explicit about what we want by
// using the custom toBeInTheDocument() matcher
expect(getByText('Howdy')).toBeInTheDocument();

The additionl matchers let us write our tests using language that’s closer to how we would naturally speak:

// test that a button is disabled
expect(getByText('Call to Action').disabled).toBeTruthy();

// with @testing-library/jest-dom we can move our assertion to the end of the
// line to match closer to how we would say it out loud
expect(getByText('Call to Action')).toBeDisabled();

Writing our First Test

Not all tests are going to be as easy as checking whether or not some heading text exists so let’s explore building a component that has a little bit of functionality.

// src/App.js
import React from 'react';

export default function App() {
  return <h1>Welcome to an Example</h1>;
}

And let’s write out our test from before to make sure everything is working (with a little line by line documentation so we understand what’s going on):

// src/App.test.js

// First we have to make sure we're importing React for our component
import React from 'react';
// `render` is the function we need from React Testing Library so that we have
// access to all of its query methods. 
// `screen` will give us access to any queries that we need to use to find
// elements on the page (e.g. `getByText`)
import { render, screen } from '@testing-library/react';

// Finally we need to import our component so that we can render it!
import App from './App';

// The it() and expect() structure comes from Jest. I like writing out my tests
// as though 'it' is a part of the sentence, so: "It renders a welcome heading"
it('renders a welcome heading', () => {
  // First we render our component
  render(<App />);

  // Then we use `screen` to query that our heading is in the document. We want
  // to use `getByText` here and not `queryByText` so that it will error if it
  // is not found (plus the error messaging will be super useful to us!)
  expect(screen.getByText('Welcome to an Example')).toBeInTheDocument();
});

Behind the scenes, getByText is searching for an element with that exact text in it and then we’re testing that that element is inside of the document. If it can’t find the element it will error.

Interacting with our Component

Not all of our components are going to be that easy to test. Let’s say there’s also a button on the page that a user can click which fires a callback to a handler that we pass in as a prop:

// src/ConfirmationDialog.js
import React from 'react';

export default function App({ handleClick }) {
  return (
    <div className="confirmation-dialog">
      <p>Please confirm that you understand that this is a test.</p>
      <button onClick={handleClick}>I Understand</button>
    </div>
  );
}

For this component we can test for two things:

  1. That our paragraph is rendering as expected
  2. That our button behaves as expected
// src/ConfirmationDialog.test.js
import React from 'react';
// Note that we're now also importing `fireEvent`
import { fireEvent, render, screen } from '@testing-library/react';

import ConfirmationDialog from './ConfirmationDialog';

const handleClickMock = jest.fn();

it('a paragraph describing the dialog to the user', () => {
  render(<App handleClick={handleClickMock} />);
  expect(
    screen.getByText('Please confirm that you understand that this is a test.')
  ).toBeInTheDocument();
});

// This test is doing double duty: by firing a click event on an element that we
// use `getByText` to find we're also able to assert at the same time that our
// button has the text that we expect and is in the document.
it('renders a button that fires a callback when clicked', () => {
  render(<App handleClick={handleClickMock} />);

  fireEvent(screen.getByText('I Understand'));

  expect(handleClickMock).toHaveBeenCalled();
});

DRY tests and working with props

Components that are more complicated or that have more props to test for may get a little unruly when you need to repeat things over and over again. I’ve found a few different patterns that help clean this up a little bit.

Pull out render() into a separate function

const renderDialog = props => render(<Dialog {...props} />);

it('renders a message for Canada', () => {
  renderDialog({ canada: true });
  expect(screen.getByText('Message for Canada')).toBeInTheDocument();
});

Define common props in a separate variable

const defaultProps = {
  canada: true,
  disabled: false
};

it('renders a message for Canada', () => {
  render(defaultProps);
  expect(screen.getByText('Message for Canada')).toBeInTheDocument();
});

describe('when we are not in Canada', () => {
  const props = {
    ...defaultProps,
    canada: false
  };

  it('renders a message for non-Canadians', () => {
    render(props);
    expect(screen.getByText('Message for non-Canada')).toBeInTheDocument();
  });
});

More Resources

If you need to test your React components I can’t recommend React Testing Library enough! The post only touches on some of the concepts that you can utilize when testing your components, check out some of these resources to keep going:

If you’re looking for full end-to-end testing with a browser and screenshotting you might want to checkout Cypress.