React Hooks broke my tests, now what? — Part 1

A guide to surviving a migration to functional components and hooks

Photo by Joshua Aragon on Unsplash

React Hooks are pretty great, right?

Perhaps, due to the hype or just because you want to improve your solution readability and keep up to date, you decide to migrate all your code to use Hooks.

You spend hours refactoring your class components to functional components, remove all the lifecycle methods, and eventually, you start using Hooks everywhere.

Finally, when everything starts working as expected, you decide to run your tests.

Oh no no no!

Suddenly you start seeing warnings and failed tests everywhere. Now what?

Now you have to make a decision:

Delete all the failing tests and rewrite them properly.

Just hammer them out and wait for the next time they fail.

The last one is not the decision you should make but, it ends up being one of the most made perhaps due to the fact you’re dealing with a deadline, and your feature needs to be delivered as soon as possible. Doing this certainly will come back to bite you in the future because you are only delaying your issues and at the same time you’re adding even more.

Through this guide, I’ll show you how writing the wrong tests for a Counter component can cause you lots of headaches, when refactoring to functional components and hooks, if your previous tests focus on implementation details.

First of all, let’s start by knowing what testing implementation details mean.

What testing implementation details mean?

Implementation details are the things which the users of your solution, won’t use or interact.

Let us see the following code:

Class Component Counter

Here we have a simple Counter, that you can increment and decrement, written as a class component.

Now let’s see the respective tests:

Class Component Unit Tests

Refactoring to Hooks

Now, let us say that we’ve decided to refactor that counter to use Hooks, and consequently change it to be a functional component. The following code would be what we would probably get.

Functional Component Counter

So, this became so much more simple than the example above. The code is smaller, and it’s way more readable.

This was a simple refactor, so we would expect that the tests should still pass right?

Nope and the type of the errors are consistent on all tests.

So lets put aside the fact that to use Hooks, we need functional components and to manipulate state we need class components.

The first mistake on the tests above was focusing on manipulating the state directly. That should be avoided at all cost because it fits in the implementation details category.

Instead, we should be interacting with the component directly as a user would.

If you intend on continuing using enzyme, then try to avoid shallow rendering on your tests.

Shallow rendering will not render child components. This means that, in our tests, we won’t be interacting with the component as the user would.

Enzyme exposes lots and lots of things that allow us to test our components. If we aren’t careful we may end up improperly testing our components.

It was to avoid these issues and to encourage everyone to test your components the right way that the testing library was built.

Now, let us rewrite our tests to use the react testing library.

Rewriting our tests

Unit Test using Testing Library

The first thing we see on these tests is that there are no state references, they are more clear on what we intend to test, and the ways to query the components are simpler.

Let’s review the differences between tests:

For default values, rather than checking if the state is set, we check if that value is in the DOM. For incrementing and decrementing, instead of simulating the state changes and checking if the value of the counter changes, we find the button responsible for the event being triggered and fire a click on it. Then we validate if the previous element we found increased and decreased value based on the pressed button.

It’s important to mention that, if we had written tests like these, in the beginning, we would have zero issues on a refactor to hooks because they are free of implementation details.

Conclusion

With this, we conclude the first part of this guide.

Our notion of a unit may need to be adjusted, as some people disagree with calling a test that renders to the DOM a unit test (even though that component is only being tested as a unit and each external dependency can be mocked to insure isolation). Despite all that, it’s important to understand the confidence and value produced by these tests. Focusing on the way the user will interact with our components will keep us focused and ensure that our tests will only break when they are supposed to.

The next parts of this guide will focus on testing changes from the lifecycles to useEffect Hook and migrating from Redux to a useContext approach. Hope you enjoyed and stay tuned for the next parts!