Hot React Testing Practices
From my anecdotal experience, React is by far the easiest framework to test your application code. However, it’s still easy to muddle up your tests. This article outlines some practices and guidelines for testing React components well.
There are many test runners and frameworks that can help; I’ve found Jest and Enzyme to work great together. Most of this article will be based around those, but, the underlying concepts should work with any test runner and framework.
When I’m approaching testing within React, there are three guidelines I live by:
- Tests must be maintainable
- Test the UI/UX
- Tests must not overlap on code paths
Why do these matter? Why are these the guidelines? It’s simple, and comes down to what could go wrong. Tests are instant feedback when your change causes a bug. These three ideals ideally make error reporting predictable and sane. Let me explain these guidelines:
An unmaintainable test suite is equivalent to no test suite. The reality is another dev might delete tests when they break, or rewrite them and cover up the bug they just created. This generally happens because the tests are unintuitive.
The UI is what matters to the user. If it looks broken, it is broken. Historically, this sort of bug was hard to test; luckily, React makes this easy with its virtual DOM implementation. But this guideline is here because it matters to the user.
The UX is basically the same as the UI. To quote the great Snow Patrol:
If it looks like it works and it feels like it works, then it works.
One of the most confusing things with tests is when a single code change causes several tests to fail. How could that be? This is generally because tests overlap on code paths.
Hot! Hot! Hot Practices!
These three guidelines are easier said than done. Some code paths of an application just aren’t easy to test, and therefore maintenance on that path will be rough. Other times, it’s just hard to keep a mental note of where tests could overlap. However, I think if we can become disciplined in those ideals, our tests will be easy to reason about and address when one fails.
To aid in writing tests, the following practices might help:
- Separate your testing concerns.
- Use Jest snapshot testing for your UI.
- Use Enzyme for testing your UX.
- Use any library to increase test legibility.
For the remainder of this article, we’ll be working off this component for being tested. A simple component which renders a number and increments when the button is clicked:
I’ll try to tie back each practice into how it improves on the three guidelines. This list could probably be ever-growing, but it’s currently what shapes my mindset and drives my test-writing philosophy.
1. Separate your testing concerns.
Maintenance | Zero test overlap
Maintainable tests are short, to the point, and legible. Test files are generally 2–5x longer than the original component. This means a relatively simple component of 50 lines could have proper test coverage within 100–250 lines of code. A complex component could result in very large test files. Remember the last time you saw a file that was 250+ lines long? You rewrote it because it sucked. Large test files are unacceptable because they are unmaintainable.
With React components, there are always two things that are tested:
- The HTML output based on props and state.
- User interaction and how it affects state.
Those lines seem pretty clear, so why not break up our tests like that? So let’s do that! Create one test file, which ends or somehow includes UI in the name, and the other must include UX. These are your boundaries.
- The UI tests should only assert the rendered output based on props and state.
- The UX tests should only assert state after interactions.
If you strictly follow this over-arching rule, you should easily be able to achieve the Zero Overlap concept, and help towards the Maintainable concept.
I’m going to leave this point relatively vague as it sets up the next two points. But really the main idea is to separate tests by UI and UX.
2. Use Jest snapshot testing.
Maintenance | Zero Overlap | The UI
This is a really cool new feature that Facebook has created. Testing your component’s output UI is arguably the most important aspect. However, it’s been a trade off — depth to clarity. To get the most depth of testing, you would have to manually assert each aspect of the tree: find a div, with a class of foo, with 2 spans in it, each with a number as the value… That’s simply not realistic in terms of tests, especially with complex components. To have truly proficient tests, we should aim to test the entire tree’s output.
Jest’s new snapshot testing feature is great for this. It lets us do full-depth assertion without having to write unmaintainable tests. Let’s compare the approach of Enzyme asserting against Jest snapshot with our Incrementor component
This is what it would be like to manually test the tree.
As a test, I’d encourage you to forget about the HTML output we gave you, and try to predict what Incrementor will render based on that test. How long did it take you to decipher? This sort of test is not scalable.
If every test you wrote based on prop was written like this, no one would take the time to read the tests when they break. They would either just rewrite or erase them. Either way, it’s bad. Rewriting the test might hide a bug that was created with the change that broke the test.
The alternative, and equally strong test approach, is Jest’s snapshot feature. Here is what that approach could look like:
That’s it! We reduced n lines to 1. The benefits of this approach increase the depth of testing beyond what the average developer will write. It increases the legibility of the test and the scalability. And therefore the maintainability.
Just so you know how the snapshot testing works: Jest will write the JSON output of your component to a file which is expected to be committed to source. These files let Jest compare between commits from the components output. For more information, read on here.
3. Use Enzyme for testing your UX.
Maintenance | The UX
Now that we’ve covered the best way to write your UI tests, let’s address the UX tests. To prevent overlap, never use snapshot or any sort of rendered output assertion in your UX tests. This is what your UX tests should do:
- Simulate UX.
- Assert the expected functions / state changes were triggered
That’s it. If you test the output, then you are overlapping tests. So just ensure that the changes you expect do occur. If it’s a Flux store update, ensure the proper store method is called. If it’s local state, ensure the state is updated as expected. For our component (Incrementor), the onClick of the button should simply bump the value in state. Let’s see this with Enzyme.
4. Use any library to increase test legibility.
This one is a little obvious, but use any library you can to increase the legibility of your code. There are tons of helper libraries for every test runner.
Here are a few that could help you out:
Here is a quick example of how much cleaner a test can read with these helping matchers.
Let’s be disciplined.
And that’s it. Now it’s time for you to learn to be disciplined in separating your testing concerns. As a quick review, this is the practices for writing great unit tests for React applications.
- Write maintainable tests by separating your UI/UX testing into their own respective files. UI tests should never simulate any UX, but only assert the output based on props and state. UX should never assert the output, but only ensure state is properly changed.
- Use Jest’s snapshot feature to write cleaner UI tests.
- Use Enzyme for your UX tests.
- Use as many helper libraries to make your tests as concise and expressive as possible.