Introducing tests in an existing codebase

If you have not experienced it before, you almost certainly will some day in the future: you join in on an existing project, but it has no test coverage to write home about, unit test or otherwise.

We’ve all learned about the need for automated test coverage in your code, either via an education (official or otherwise), or through experience. So how do you deal with projects without automated tests?

Missing tests are definitely a technical debt for the codebase. Every time you have to change code, you will have to pay the interest on that debt. This will manifest itself in bugs being introduced by changes and the extra time needed to properly change the code. Whether or not it is worth to pay this debt off will depend largely on the project. Is it still in heavy development, or will development soon be limited to essential bugfixes? In the case of the latter, introducing tests may never pay off.

So should you just accept this debt and move on? Definitely not!

Sure, there are cases where the cost to pay off this technical debt will be higher than the total interest you will pay over the rest of the project’s lifetime. This will, of course, always remain an estimate. In such a project, simply paying the interest may be the best thing to do. Give up on the code ever being completely tested, and only make the changes when necessary.

Having said that, there are some excellent opportunities where you can get the test coverage on your codebase up, with minimal costs.

When building new components

For new features, make sure you write the relevant code in a testable fashion. Even if there already is a class in the code that would be a logical place for this new code, consider if it’s not better to split it up. After all, starting out with a clean slate is one of the easiest ways to properly introduce test coverage! When you write this new module, do this in a test-driven way.

This will not change the amount of lines that have no test coverage. However, it will still increase the amount of tested code in the project. Pick your battles! Even a small amount of the code being tested is infinitely better than none of it.

When fixing bugs

If you’re fixing a bug in a codebase with good unit test coverage, you can often easily reproduce it as a unit test. Then you make the test pass, fixing the bug in the process. This results in happy users, both now and in the future; Since there is now a unit test, that code will be easily tested for regressions.

In the projects we’re talking about now, however, you will not yet have a unit test set up for this class. It’s probably barely unit testable in the first place!

However, it can be valuable to still write a test for the case. Try to set up a sort of framework to test this class. Take the time to mock out every dependency of the class with data objects filled with bogus data and mocking frameworks for other dependencies. Do this in a way that’s easily reused, however. Don’t try to make it a masterwork of clean code; Accept it will be far from the most beautiful test you’ve ever written. Make it overly verbose!

Make sure this framework can be used for other tests as well. Lower the barrier of entry to write more tests for this particular part of the codebase in the future. If possible, take the time to also write a few tests for the very basic cases this code will have to test for.

If you will ever have the chance to refactor components like these to be properly testable (which is usually quite a large up-front payment to lower your technical debt), these tests might all have to be rewritten as well. While this might seem a waste, they will still make it easier for you to verify that the refactored code still behaves like the old code did. Which is, after all, one of the goals.

Concluding

Writing tests takes time, and time is money. Furthermore, when changing existing tests just to introduce tests, you’re not actively providing new value for your customer (which will surely make your product owner very nervous!). The cases above are definitely not the only opportunities to improve the quality of an existing codebase. However, they’re ones where you’re clearly adding business value while doing so.

Continuously making large changes in a large codebase without proper test coverage, however, brings significant costs in the long run. There are too many codebases out in the wild with no or bad test coverage, and you need to consider your options to solve this, in small steps.

When touching code that you consider serious technical debt, just keep the rule of the boyscouts in mind: ‘always leave the campsite a little cleaner than you found it’.

Erik Steenman

Erik Steenman is sinds 2018 bij Profit4Cloud in dienst als Software Engineer met specialisatie AWS. Erik is AWS-CSA/A en Azure Developer Associate gecertificeerd.