React testing is trending toward functional testing: test your app the way a user would use it. The tests go through user flows, and are decoupled from the code (testing behavior over implementation). This is fantastic when it comes time to refactor your code, or update the code without changing the behavior. Since the tests are tied only to the behavior, there’s no need to update the tests. Champagne all around! 🍾
This leaves a question, though: is there any space left in React for unit testing (tests that isolate one unit of code, often a function or component)?
Unit tests can be functional tests
Functional testing and unit testing aren’t mutually exclusive. Functional testing means testing a certain flow of your app, which could be contained in a single component.
For example, say you have a component, Quote, that displays a pithy quote and links to the author’s website. Some authors might not have a web site, though (Abraham Lincoln was notoriously bad at keeping up with web technology). Your tests need to check that the quote renders correctly for both situations: authors who have a link, and those who don’t.
These are functional tests for a user experience: they confirm the author’s name is either a link to the right destination, or plain text without a link for author data without a web site link. But they’re also unit tests for the Quote component. The test can render the Quote component with the quote and author data as props, and the tests are isolated to that one component. So here you’re writing unit tests simply because all the code for your functional test is contained within one unit.
Okay, so this example seems to be “unit testing as a technicality.” You’re actually doing functional tests that just happen to be unit tests. Are there any reasons you might want to unit test deliberately? Read on, my friends.
Unit tests help diagnose other failing tests
Functional testing is awesome when it comes to refactors (see comments about champagne above). But, when you test behavior and not implementation, your tests are intentionally decoupled from your code — which makes it harder to diagnose failing tests.
In a large, complicated project, there could be dozens or hundreds of lines of code responsible for a certain functional test, and narrowing down the cause of a failure could take a fair amount of time and effort. In some cases, you could potentially spend more time tracking down test failures than you would maintaining unit tests.
If there is a likely potential point of failure for one or more functional tests, you might consider making a unit test to cover it. This way, you can easily zero in on what is causing the functional test to fail — or eliminate a potential reason the functional test is failing.
As a simple example, imagine an app where someone can look up laws about backyard chicken coops for a particular city. The user selects a state from a drop-down menu, which populates a second drop-down menu with cities in that state that have such laws. Some states (I’m looking at you, California) have a lot of cities, so the city menu is broken into sub-menus by alphabet if there are more than 20 cities in the state.
You may write a functional test where a user navigates to the search page, selects a state and a city, and sees the correct legal information. The most complicated part of this test — and most likely point of failure — is the appearance and content of the second drop-down containing the city options.
So here you might want to write unit tests to support your functional test. These tests could render only the form element and ensure that the second menu is not there when you load the page, that it contains the correct data for a state with 20 or fewer cities that have ordinances, and that the menu paginates properly for cities that have more than 20 cities.
This type of unit test makes it easier to diagnose failing functional tests. If all of these unit tests pass and your functional test fails, you know that the problem lies elsewhere. But if you find a failing unit test, you know what you have to fix in order to get the functional test to pass.
Unit tests confirm data sent to the server
Your functional React tests generally don’t involve your server. It’s common to use a tool like Mock Service Worker to simulate responses from the server, but how do you know that your app is sending the correct data to the server?
For example, say you have a Redux action creator that processes form data and creates a JSON patch to update an entity on the server. You may want to write unit tests for this action creator to make sure that the JSON patch includes all the necessary data (and none of the extraneous data) for particular sets of form inputs.
This is testing implementation to be sure! The user neither knows nor cares about the exact data being passed to the server. However, functional tests usually don’t involve the actual server; that’s reserved for end-to-end tests. So this type of unit test will highlight where an end-to-end test is (or isn’t!) breaking down — or even better, prevent you from pushing code that breaks end-to-end tests in the first place.
Unit tests for edge cases that don’t affect render
It doesn’t always make sense to render the whole app (or even a particular component) to cover all the edge cases for a helper function.
Imagine you have a word game app that needs to check how many letters a guess has in common with the “secret word.” You write a helper function for this, which can easily be unit-tested to make sure it does the right thing when (a) the guess has more than one of the same letter, (b) the secret word has more than one of the same letter, (c ) the word has all the same letters as the secret word, (d) the words have zero letters in common.
The app does the same thing with the match count regardless of the above conditions, so it’s not necessary to render your JSX to test edge cases for this particular function. You will want to test this logic to make sure any added code doesn’t introduce errors, though. So here, unit tests of this function are the best way to go.
I have switched mostly to functional tests along with the prevailing best practices. However, I still use unit tests when:
- my functional tests happen to test only one unit
- there’s a likely point of failure in a longer functional test
- I want to confirm that the correct data is sent to the server
- I’m testing edge cases for a helper function that don’t affect rendering
Do you agree or disagree with me? When do you use unit tests? Let me know in the comments!