How to use React Testing Library to rewrite an Enzyme Component test

A mini-rewrite case study of two testing libraries

Luke Ghenco
Flatiron Labs
7 min readJan 16, 2019

--

The Dream of 100% Test Coverage

Learnings from a recent conversion

At Flatiron School, we have been adding a ton of new test coverage using Kent Dodd’s React Testing Library. Check out my other blog posts on React testing (Creating Readable Tests Using React Testing Library & Testing Async React Hooks)for some awesome tips on using the tool (I know, I know! That was a shameless plug). As we’ve been improving our overall front end (FE) test coverage, there have been certain React components using Enzyme tests that needed some attention.

Since I’ve been learning a lot in this conversion process, I wanted to share some solutions so that you too might be able to convert tests around a React component from using Enzyme to using React Testing Library. I also wanted to share some cool bonuses you might get in the process of conversion (improved test coverage anyone?).

Quick Setup Info

This is using code that has been slightly modified and simplified from our code base. To get this running locally, copy the code over to a simple Create React App and add the React Testing Library.

The Component(s) we are testing

The core component we will be testing is for a textarea input that uses a secondary child component to render an error message. Here is what the code looks like:

Step: 1

The original Enzyme test

This is what our current tests look like:

Step: 2

So before we start refactoring and talking about changes, we should state some facts about our tests using the Jest coverage tool. Running yarn test — coverage (or npm run test — coverage, if you use NPM), I get the following info:

  1. 54 lines of code in test file.
  2. The code coverage for TextAreaInput.jsx is currently 90% on statements and 62.5% for branches.
  3. The code coverage for required InputError.jsx is currently 42.86% on statements and 0% for branches.
  4. The overall code coverage for both components is currently 72.2% on statements and 50% for branches.

{mount, shallow} vs render

With Enzyme we have the concepts of Mount and Shallow for rendering component. This is a really good Gist on the differences: Difference between Shallow, Mount and render of Enzyme. Basic answer: Mount renders everything and allows all lifecycle methods to be used & Shallow only renders the component with limited functionality and no child components. This allows for basic unit testing, but the UI never really works independently of itself, so we’ve found that we get brittle test coverage when we used Shallow in the past.

The tests we have above only use Shallow, so each component is tested individually and does not signify that the component is working together as a whole in the UI.

The Render function we will be using from the React Testing Library works similarly to Mount and will render all of the child components for us as well as have access to lifecycle methods (our example code uses stateless components so this is a non-issue for the following tests) in its mount and un-mounting process.

Looking for similar test patterns

So the first thing I like to do is try to find testing strategies that work in Enzyme and React Testing Library (i.e. Integration Testing or as a user would interact with the component).

So for example, let’s refactor this portion:

Step: 3

With the following:

Step: 4

We can then continue the trend as we move through the code, so we will be able to go from our original test (see code in step 2) to our new tests using React Testing Library:

Step: 5

Just a couple of notes on this before we discuss the improvements. You will notice I used the getByLabelText() function to find a textarea tag. One of the cool things about React Testing Library is how it expects and pushes you to use best practices. An input should have a related label attached to it for accessibility reasons. To make this happen an id attribute on a textarea tag must match the htmlFor attribute on a label tag.

So what did we gain from the code above?

What stands out in my mind is more readable code, due to not having to test as many implementation details. We searched for error message text to be rendered, which verified that the InputError component was working correctly, versus only checking to see if the InputError component was mounted (i.e. const node = this.wrapper.find(InputError)). This allows us to have easier and better refactoring powers later, as it releases us from having to rely on the InputError component. As long as we successfully handle rendering the error message prop in some form or another the user experiences the same results.

This is a bit subjective, but I find that the description blocks can be rewritten to be more human readable. The description seems to map closer to the actual code in the specs.

Lastly, code coverage has improved. Let’s compare specs again using yarn test — coverage:

  1. 51 lines of code in tests. (could be less by just using getByText() to find element and not checking for it to be defined, as that is redundant)
  2. The code coverage for TextAreaInput.jsx is now at 90% (no difference) on statements coverage and 70% (+7.5% improvement) for branch coverage.
  3. The code coverage for required InputError.jsx is now at 100% (+57.14% improvement) on statements coverage and 100% (+100% improvement) for branch coverage.
  4. The overall code coverage for both components is now at 94.4% (+22.2% improvement) on statements coverage and 80% (+30% improvement) for branch coverage.

Looking for test paths that we didn’t cover in Enzyme

That was an incredible improvement in readability, simplicity, and code coverage all with less lines of code. Now let’s focus on improving our test coverage to 100% and testing out all of the branching paths that are component handles.

Branch paths we are not testing currently:

  1. What is the class name for the textarea tag if no className prop is present?
  2. How does it connect label tag to textarea tag if id prop is not present?
  3. How does it disable the textarea tag?
  4. How does it handle placeholder text?
  5. Will it render the value prop inside of the textarea tag?

There are probably more paths I’m missing, but this seems like a good start.

Let’s work on the checking for the default behavior of the class name attribute for the textarea tag. To test this, we should be able to add the following code:

Step: 6

Then to check how a textarea tag is connected to a label tag using the id and name props. We will add a test for two different branching paths:

  1. When an id prop is present:
Step: 7
  1. When an id prop is not present, but the required name prop is:
Step: 8

Checking for a disabled textarea tag will be pretty similar to the tests for connecting label tag to textarea tag. The two different branching paths will be:

  1. When disabled prop is present.
Step: 9
  1. When disabled prop is not present.
Step: 10

To check that it renders a placeholder we should be fine with just one test.

Step: 11

For the last test, we will make sure it renders the value prop. This will also add verification that an event handler is present on the textarea tag to make it dynamic instead of only being a read-only input.

Step: 12

Checking out the final code coverage using the new tests

So now that we’ve completed our new test code, it should look like this:

Step: 13 — Final

We’ve increased the amount of lines of code from 54 -> 106, but when we run yarn test — coverage, we get a much happier score card:

100% on everything!

In retrospect, for a real world scenario, the InputError component is probably used by more than one parent component, so additional test coverage would be recommended. My goal here was to show how we could improve test coverage of two components at once by just testing the parent component. I believe that was successful.

Closing Thoughts

We’ve found great success so far in improving our overall code coverage on our FE code in the last few months since switching over to React Testing Library and I can not recommend it enough. The ease of use in adding tests to new components and converting old tests to use React Testing Library can not be overstated.

With that said, Enzyme is a great tool, and not every team will be able to switch over. For those teams, I would highly recommend implementing React Testing Library’s guiding principle “The more your tests resemble the way your software is used, the more confidence they can give you,” and only use Mount to verify that your components are working as a unit.

Thanks for reading! Want to work on a mission-driven team that loves React and high confidence testing? We’re hiring!

Footer top

To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.

Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.

Footer bottom

--

--