Test-driving React Hooks

Last time, I talked about what React hooks are, how they enable deeper use of functional components, and why you might want to use functional components over class-based components. This time, I'd like to focus on testing React hooks, specifically test-driving them into an application.

I'd like to note up front that I'm going to be assuming a couple things in this article. First, I assume you have npm installed and know how to use it. And second, you know how to create a new react application (I recommend using create-react-app to get started if you don't know how or just want to get something started quickly).

Testing Options for Hooks

Before getting too far, I'd like to start with a quick primer on testing React. I plan to write a much bigger article about this, but an introduction to the main players should be sufficient for today.

The Players

There are three main players for testing in React:

  • Jest
  • Enzyme
  • react-testing-library

Jest is a very popular JavaScript test runner. It provides many utilities for testing your applications, and I highly recommend it.

Enzyme is a testing utility for React. It provides the ability to render the components in your application and search for elements in the render tree.

react-testing-library is another testing library for React that sells itself as a replacement for Enzyme.

For this article, we will be using Jest and react-testing-library. I am far more familiar with Enzyme, but, unfortunately, as of the time of this post it does not fully support hooks at this time. Follow this GitHub issue for more information and updates on its support for React hooks.

Getting Set Up

Assuming you have a new React project, we just need to add a couple things to get going. First, check your package.json file to ensure you have at least version 16.8.0 of React, as that is the first version containing hooks in the stable build. Next head to your terminal and add the dependencies for react-testing-library:

Using npm

> npm install --save-dev react-testing-library

Using yarn

> yarn add --dev react-testing-library

Now, I would suggest you read the docs for yourself, such as they are in their current state. I had some difficulty understanding what I need for testing this hook. The relevant links are the following:

Driving the Hook In

Great, so with that done, we can drive in this hook. The hook we'll be using, as I mentioned before will be the useState hook. And we'll be making a simple counter with a button to increment it. Nothing fancy for this; using the testing library is the important part here. And so, we begin with a test in a file called HookCounter.spec.js:

import { render, getByTestId, fireEvent } from 'react-testing-library'

import HookCounter from './HookCounter'

describe('HookCounter', () => {
it('should increment the counter', () => {
  const { container } = render(<HookCounter />)

  const increment = getByTestId(container, 'increment')

  fireEvent.click(increment)

  expect(getByTestId(container, 'count-display').textContent).toBe('1')
})
})

There's a lot going on there, so let's take a closer look. First, we import the things we need from react-testing-library. We get a function to render the component, a function to find the button we'll be using to increment the counter, and, finally, a way to simulate a click on that button. Then we use that render function inside our test - designated by the it - and deconstruct the rendered container out of the object that render function produces. Next, we find our button by the test ID we'll set up later and fire off a fake click event on it. That should simulate the behavior we want, namely that clicking the button increments the counter. Finally, we use Jest's assertion expect to assert the content of the count display has been changed from 0 - the initial value we assume will be used by the component - to 1.

Cool, so we run the test and that should fail.

And it's a simple failure. We haven't created the HookCounter component yet. That's easy to fix:

import React from 'react'
          
export default const HookCounter = () => <div></div>

That will give us enough to get through to the failure to find our button. That's a good start. The empty div we had before won't cut it, anymore. Let's add some content:

import React from 'react'
          
export default const HookCounter = () => (
<div>
  <h3 data-testid={'count-display}>0</h3>
  <button
    data-testid={'increment'}
    type="button"
    style={{width: 260, height: 50, backgroundColor: '#39CCCC'}}
  >+
  </button>
</div>
)

Perfect, that gets us to the assertion failure. I worked ahead a little, adding the count display and doing some minor styling so we could get to that assertion failure faster.

Now, the assertion says still have 0 for the display. Why's that? Well, we haven't started using the hook, yet! So, let's get that in there. To do that, we need to import the useState hook and create a click handler:

import React, { useState } from 'react'
          
export default const HookCounter = () => {
const { { count }, incrementCount } = useState({ count: 0 })

return (
  <div>
    <h3 data-testid={'count-display}>{ count }</h3>
    <button
      data-testid={'increment'}
      type="button"
      style={{width: 260, height: 50, backgroundColor: '#39CCCC'}}
      onClick={() => incrementCount({ count: count + 1 })}
    >+
    </button>
  </div>
)
}

And that should do it. We should have a passing test now. Notice that instead of naming my deconstructed values from useState stateand setState, I changed their names as I deconstructed them to make more sense in the app.

Conclusion

I know this was a pretty simple example, but I want to make the business logic simple in order to focus on the testing aspects. The code I originally wrote to figure this out can be found in this repo. I was working on more than just one thing there - including TypeScript - but I liked what I ended up with, and maybe the steps of the commits will be helpful.

If you have any comments or suggestions - or maybe just liked the article :) - please feel free to get in touch with me via Twitter using the link below. That's all for today. Thanks for reading, and I'll see you next time.