4 Best Practices for Retroactively Adding Automated Tests

If you’ve been programming for a while, there’s a good chance that you’ve worked on a project without a suite of automated tests.

At Atomic Object, we’re regularly hired to enhance existing codebases. Before starting on any new features, we often take the time to retroactively add automated tests. Having a full suite of automated tests allows us to confidently make changes to the application, knowing that it will work as expected.

Adding tests retroactively isn’t ideal, but it’s still better than not having any! It might seem like a pain at first, but here are a few ways to make this process easier and more impactful.

## 1. Start Small
Fully testing an entire application can seem like a daunting task, especially if that application has existed for quite a while. So start small with some simple unit tests. Think very small here, the bare minimum necessary to add a test case and execute it with the test runner.

Whether you’re familiar with the tools or not, this will help get you started and prepare the project for tests that actually assert meaningful behavior. Unit tests should be quick to write, quick to execute, and quick to modify/remove as the application changes.

## 2. Utilize Code Quality Metrics
Many software developers dread the words “code coverage.” Larger organizations often use this performance metric to determine the “health” of a codebase. While not necessarily a bad practice, this fixation on 100% code coverage is more harmful than helpful. It’s an important metric to utilize, but it’s not the only metric that matters.

Instead of focusing on the 100% goal, use code coverage reports to prioritize which parts of the app get tested first. If you do want to focus on the code coverage percentage, use it as a way to track your progress and communicate with your team. It’s probably a good idea to set some expectations about the specific percentage you’re trying to achieve at this stage of development.

## 3. Avoid Premature “Bug Fixes”

xkcd: Fixing Problems

After writing a handful of unit tests for various classes and functions, you’ve probably noticed some potential bugs in the code. The point of adding tests at this stage is not to go through the application with a fine-toothed comb and “test all of the bugs out.”

If this is legacy\* software, there’s a good chance other projects rely on its current behavior. Fixing a bug in this system without understanding the full picture might result in cascading effects for all of the other systems that rely on it. Arlo Belshee spoke about this topic at length during [his Deconstruct 2017 talk](https://www.deconstructconf.com/2017/arlo-belshee-i-find-bugs-too-boring-to-write).

> “I need to be able to guarantee 100% that not only did I not accidentally introduce a bug, I didn’t accidentally fix a bug that I don’t even know exists.”
>
> \- Arlo Belshee

Instead of fixing the bug right now, add tests that assert the current behavior. You can always add a `TODO` comment or track it in a product backlog to revisit it later once the test suite is in place.

\* It’s probably not a bad idea to treat all software as “legacy” software, considering how quickly things change.

## 4. Set Up Continuous Integration
Once you have a handful of automated tests, it’s time to actually automate them! Running the tests on your machine is a great start, but this relies on you remembering to run them as you make changes. If you forget to run them, there’s nothing in place to let you know if you break something by accident.

Services like [GitHub Actions](https://github.com/features/actions) and [CircleCI](https://circleci.com/) allow you to define an execution environment and a series of commands to execute in that environment. This can be especially helpful for building/deploying an application, but it can also be used for automatically running tests as you make changes. CI systems can be configured to run test suites, generate code coverage reports, and upload summaries for later viewing.

Depending on your project or which Git remote you’re using, here are a handful of the CI options you could configure for your own project:

CI Service Description
GitHub Actions The CI Service built into GitHub; useful if your project is already hosted on GitHub.
CircleCI A great option if your project works on Linux or OSX; offers great tools for defining workflows/jobs.
Azure Pipelines Good option for projects hosted in Azure DevOps.

My colleague, Brian May, recently [compared CircleCI and Azure Pipelines in depth](https://spin.atomicobject.com/2020/07/14/circleci-vs-azure-pipelines/).

These are a few of the things I try to focus on when retroactively adding tests to a project. If you’ve been in this scenario before, I would love to hear some of your own tips for making this process easier!