5 tips for dealing with untested React codebases
During my career as a web developer and consultant, I've worked with teams, mentored other developers, joined ongoing projects, and in most of these situations, one common pattern stood out often: even the fanciest organizations don't test their frontend code.
These projects behave more or less like this: they kind of work for the end user, but they are an absolute nightmare to maintain, let alone changing a single line of code with confidence.
It's not hard to understand the reason for this. Testing takes time and effort, and sometimes there is literally no time to write tests, especially for that quick hotfix we needed to ship to our users ... yesterday.
The reality is that in all these situations, even a little testing can save you and your team in the long run.
What follows are a series of guidelines to introduce tests in untested React codebases.
I want to stress out that the choice of React is merely an accident: it is the tool I'm mostly comfortable working with these days, but the guidelines presented here are applicable to any frontend library or framework.
Let's start off!
1. Extract duplicated UI components
During our day job as JavaScript developers, we are always in a rush to add features after features to the projects we work on. The client wants this, and then it wants that, and it's difficult to keep up with all the changes, let alone remember that you already have built some specific component, or some piece of logic that we now need in another part of the application.
Day after day, code accrues and begins to repeat itself. It is easy to spot these patterns (IDE are good at reporting duplicated logic) and extract duplicated units in an untested React codebase.
Any time we encounter this situation, we can apply the following process to extract and test the unit:
- identify the duplicated component
- prepare unit tests for the consumers of the extracted component: any component which uses the subcomponent we're going to touch must be put under test to avoid breaking the whole app
- prepare a unit test for the new component, and finally extract the component by following a test-first strategy
2. Add tests to any visited components
Joining a new project means it's highly likely there is already a set of components which constitute the bulk of the application.
Not every React application is a mess of tangled components. Maybe you joined a new project started by brilliant developers who produced truly reusable components but hadn't time to add proper tests. This is a common situation, understandable, but not so forgivable.
In an legacy project, when adding new features, you will likely reuse most of the work done by other developers. If these components are untested, as you use them, start by adding unit tests to any component you touch. This will likely increase code coverage by a little day by day.
My motto is: "Be like a king Midas of testing": test anything you touch. If you follow this simple rule, day by day this "little" will compound, and in no time you will increase code coverage for the entire codebase.
3. Extract and test React hooks
React Hooks are great for extracting common UI logic into reusable units of code.
However, most of the time it's easier to stuff more and more logic into a hook as the component grows, rather than designing a loosely coupled hook from the beginning.
For this reason, it's easy to find yourself with a gigantic set of useEffect
which do too many things at once, and most of the time are also duplicate of other hooks.
In these situations, you can identify "low-hanging hooks", put them under test with React hooks testing library, and then follow the same process seen in section 1:
- identify duplicated hooks
- prepare unit tests for the consumers of the extracted hook
- prepare a unit test for the hook, and finally extract the hook by following a test-first strategy
4. Add exploratory functional tests
It's hard to understand the work already being done by other developers, and it's even harder to figure out what this or that component does, if taken alone.
Not everything is lost. Sometimes, by looking at an application from the outside, we can quickly gain insights about the flow of a particular feature.
Functional tests for the UI are a great way to explore an application, and to identify architecture smells in untested codebases.
What's an architecture smell? This is a fancy term to describe tight coupling between any frontend application, and a given API/backend. Here's an example:
Try to write a couple of tests for an application or for a component you wrote earlier without thinking too much about its design. Done? Run the tests. Do you need the real backend to make these tests pass? If the response is "yes", then you have found an architecture smell.
Tools like Cypress make easy to start writing functional tests for frontend applications.
These tests are great for:
- identifying and catching regressions
- spotting tight coupled applications/components
- gaining insights about the whole application
5. Routine checkup and self discipline
Writing about testing and best practices it's easy. Applying the theory properly and consistently is another story.
When you code, try to do a routine checkup of your testing posture, and adjust your behaviour accordingly.
Resist the urge to write untested code, or at least go back to add tests when you reach an implementation you're happy with.
Thanks for reading!