How to use React Testing Library to rewrite an Enzyme Component test
A mini-rewrite case study of two testing libraries
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:
The original Enzyme test
This is what our current tests look like:
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:
- 54 lines of code in test file.
- The code coverage for
TextAreaInput.jsx
is currently 90% on statements and 62.5% for branches. - The code coverage for required
InputError.jsx
is currently 42.86% on statements and 0% for branches. - 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:
With the following:
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:
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
:
- 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) - The code coverage for
TextAreaInput.jsx
is now at 90% (no difference) on statements coverage and 70% (+7.5% improvement) for branch coverage. - The code coverage for required
InputError.jsx
is now at 100% (+57.14% improvement) on statements coverage and 100% (+100% improvement) for branch coverage. - 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:
- What is the class name for the
textarea
tag if noclassName
prop is present? - How does it connect label tag to
textarea
tag ifid
prop is not present? - How does it disable the
textarea
tag? - How does it handle placeholder text?
- Will it render the
value
prop inside of thetextarea
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:
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:
- When an
id
prop is present:
- When an
id
prop is not present, but the requiredname
prop is:
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:
- When
disabled
prop is present.
- When
disabled
prop is not present.
To check that it renders a placeholder we should be fine with just one test.
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.
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:
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!
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.