Unit testing React - What you need to know

Periodic Background Sync Cover Image

About 2 years ago, after I joined my current company, a colleague and I had the task to start writing unit tests for our React applications and increase the code-coverage of the frontend projects. I had no prior experience in this topic, but I've learned a few things along the way, including the fact that purely focusing on code coverage isn't a good idea.

I intent this article to serve as a starter guide for developers who are new to testing React components or even unit testing in general. I won't go into the technical details here, but I'll point you to articles and books that I read and found useful.

I'll also include some practical advice on how to go about testing and some common pitfalls you might encounter at the end of the article.

If you're interested in a step-by-step guide for unit testing React components, subscribe to my newsletter and send me a message via email or twitter to let me know that you're interested.

Table of Contents

Unit Testing Introduction

This section is an introduction to unit tests, which people who are new to unit testing will hopefully find useful. But even if you're already experienced, you might find something useful here.

If you want, click here to skip this section.

What is unit testing in React?

Unit testing is a testing method that tests an individual software unit in isolation. This involves verifying the output of a function or component for a given input.

For React components, this could mean checking that the component renders correctly for the specified props.

In other words, writing unit tests means that we write code that verifies that our code works as expected. We'll go over the other goals of unit testing later on.

What's the difference between unit tests, integration tests, and end-to-end tests?

Good that you ask!

Unit tests test the smallest unit possible, mocking any dependencies the component may have.

Integration tests test how multiple components work together. These tests give a better understanding of how the user experiences the application.

The downside of these tests is that it's harder to find which component caused the test to break. While failing unit tests indicate an issue in one particular component, one broken integration test can be caused by multiple components, and it's not always clear which component caused it.

End-to-end test — also called UI tests — take integration tests even further by testing the whole system from the point of view of the user. These tests don't know the insides of the system since they focus on the system of the point of view of the user.

These tests click through the application and verify that the UI matches the expected results. They are the result of automating manual tests.

To explain how these tests work together it's easier to have a look at the infamous testing pyramid:

Testing pyramid Mountain

Unit tests form the base of the pyramid because they are supposed to lay the foundation for all other tests. That's because they are easier to write, and it's best to write them as part of writing code and fixing bugs.

What is the purpose of unit testing?

There are multiple reasons why unit tests can be helpful. Some of them being:

  • Prevent regressions
  • Exercise your code
  • Faster feedback while developing

These are valid points, but I found that the main advantage of writing unit tests is how it improves the way you write code.

If you write your tests during or even before implementing a feature, it gives you a better picture of the requirements. You automatically end up writing code that's easy to test, loosely coupled, and easier to reason about.

I first read about this way of thinking about tests in The Pragmatic Programmer by David Thomas and Andrew Hunt. I didn't know this when I started writing my first unit tests, but in retrospect, that's exactly what happened.

When I started writing tests for some of the legacy code at my company, it was never only writing tests. It always went hand in hand with refactoring quite a bit of the code, so that it would be easier to test it in the first place.

For example, I'd extract functions from components into a separate file to make this code easier to test. This naturally resulted in less coupled code.

So, what's the primary intention of unit tests?

I wouldn't go as far as Dave and Thomas, and say that the only purpose of unit tests is the way you go about writing your code — it has prevented us from shipping bugs to production on several occasions.

Unit tests help you with all the points I mentioned above. They lay the foundation of every solid testing suite for a good reason.

When should you write a unit test?

There are many opinions on when you should write unit tests. The school of test-driven development (TDD) tells us to write tests before writing a single line of code, others prefer to write unit tests during or even after the code has been written.

I think that the biggest advantage of unit tests is when you write tests during or before the actual implementation, but this doesn't mean you should always follow this advice.

Generally, I found that it's a good idea to write tests in the following situations:

  • Before or during implementing new functionality
  • Before and during refactoring
  • Before fixing a bug

Tools: What you need to get started with unit tests in React

So now you know what unit testing is, you want to get started writing your first test. For that let's have a look at the tools you can and should use to write automated tests.

For getting started with writing unit tests in React you'll only need the following two types of tools. I'll explain what they are and what specific libraries you can use.

  • Test runner
  • Testing utilities

You could also write the functionality that these tools provide yourself, but let's pretend you don't want to reinvent the wheel right when getting started.

Choosing a test runner for React

Let's start with a question: What is a test runner?

A test runner is a program that executes your tests (in our case unit tests) and allows us to easily recognize when tests are passing or failing. It's an important tool for removing friction by automating the execution of our unit tests. It also allows us to make unit tests part of the CI-pipeline (continuous integration).

This removed friction will automatically lead to developers executing tests more often.

In React, the most used test runner is Jest. You can run jest in watch mode, which will run your tests each time you save a file.

Jest test runner example output

Example output of Jest

You could also use Mocha to test your React components, which provides similar features.

Utility tools for React

The most wide-spread utility tools for React are Enzyme and react-testing-library. They make testing React components a lot easier.

Let's have a look at what they offer and which one you should use.

You could also use React's test renderer, which is used for rendering React components in a test environment but lacks some of the functionality and ease of use that the other utilities in this section provide.

Enzyme

Enzyme has been around since December 2015. Since then, it's been a great addition to React, which simplifies testing the output of React components.

Its most important feature is the shallow and mount functions, which allow us to simulate rendering and then inspect the output. The difference between them is that mount mounts the component and its children in the DOM, while shallow mocks all children and doesn't mount it.

Both of them make the component go through React's lifecycles and they provide utility functions for verifying the output.

it('renders children when passed in', () => {
  const wrapper = shallow((
    <MyComponent>
      <div className="unique" />
    </MyComponent>
  ));
  expect(wrapper.contains(<div className="unique" />)).to.equal(true);
});
A typical test looks something like this.

react-testing-library

The library's first release was in March 2018. It is recommended by Facebook and aims to make testing easier.

Unlike Enzyme, react-testing-library focuses on user behaviour rather than on the implementation.

While in Enzyme the focus is on verifying the correct output for a given set of props, using react-testing-library feels more like writing end-to-end tests for components:

test('allows the user to login successfully', async () => {
  render(<Login />)

  // fill out the form
  fireEvent.change(screen.getByLabelText(/username/i), {
    target: {value: 'chuck'},
  })
  fireEvent.change(screen.getByLabelText(/password/i), {
    target: {value: 'norris'},
  })

  fireEvent.click(screen.getByText(/submit/i))

  const alert = await screen.findByRole('alert')

  expect(alert).toHaveTextContent(/congrats/i)
  expect(window.localStorage.getItem('token')).toEqual(fakeUserResponse.token)
});
Taken from the official documentation.

One important difference to Enzyme is that it intentionally doesn't support shallow rendering. You can still mock components that you don't want to render but Enzyme seems to make this easier.

Many of the tests you write with this library will probably be integration tests rather than unit tests, which isn't necessarily a bad thing.

As Kent C. Dodds, the creator of react-testing-library stated:

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

That means that the higher your test are on the testing pyramid, the more confidence you can have in them.

This makes sense, but strictly following this advice would mean that we should purely focus on writing end-to-end tests. Considering the other advantages that writing unit tests bring us, I think it makes sense to have a good combination of all categories.

Which one should you choose?

This depends on what's important to you.

Do you want to write tests that test each of your components in isolation? Then Enzyme will make your life easier.

Do you want your tests to assert the functionality of your application from the user's point of view? Then choose react-testing-library.

You could also use both of them in conjunction, using Enzyme for unit tests and react-testing-library for integration tests.

I've been quite happy with Enzyme so far but I can see that react-testing-library would be a great addition.

I'd recommend you give both of them a try and see which one suits your needs the best.

Pitfalls

Focusing on 100% code coverage

When you set the goal to be 100% of code coverage, you're missing the point of unit testing.

Tools that measure code-coverage focus on things like branches and lines covered to calculate this number. What they can't provide is a metric for the quality of your tests.

You can have a 100% code-coverage and still don't be confident in your tests.

Instead of focusing on code-coverage, it's better to start writing unit tests when you change something. This has the advantage that you don't break any code due to making your old code testable.

Your code-coverage won't increase as fast as fast, but the tests you write will be more valuable.

You can further increase the value of your tests by thinking of edge cases and unexpected inputs that exercise your code.

Test-driven development (TDD) without a direction

Last year, I was working on a task to create the splash screen of a PWA programmatically. Since we were using Cloudinary for storing our images, I could achieve this by modifying the image URL. Cloudinary would interpret the URL and transform the image accordingly.

Having read a few articles on TDD, I wanted to put my knowledge to work and started writing the test before implementing the function that would create the URL.

It was great at first. I wrote a small test that would fail and then write just enough code to make it pass. In the end, I had quite a few tests with green checkmarks.

The only issue was that when I tested it in the browser, it didn't work at all.

I was working for hours on those tests just to find out that my assumptions about how Cloudinary's API worked were wrong.

After this I change the code, using trial and error, to make it work in the browser first and wrote the tests for this afterward.

The takeaway of this is that it's nice to write tests first, but only if you know where you're going (a lesson I could've learned from The Pragmatic Programmer before).

Conclusion

I hope I could give you a better overview of unit tests in React and that you learned from some of my mistakes.

I'll leave you a few links and resources where you can continue reading.

If you're interested in more articles about React, Progressive Web Apps and web performance, I recommend you subscribe to my newsletter, which comes out every two weeks.

Further reading