Micro Focus is now part of OpenText. Learn more >

You are here

You are here

It’s never too late to add unit tests to legacy code

Gene Gotimer DevSecOps Senior Engineer, Steampunk

Most developers believe in the value of unit tests, even if they don't always like writing them. Most of them are okay with writing unit tests while doing new development work or practicing test-driven development (TDD), but it's easy to be disciplined with greenfield development.

For existing code, many developers dismiss adding unit tests—it is already too late, in their minds. That's a shame, because brownfield development is where unit tests really pay off, elevating legacy code into battle-tested code. They allow routine maintenance to be performed routinely. Changes go from a risky upheaval to a commonplace activity.

Unit tests document how developers intend for the code to behave. Of course, you can't always be sure of the developer's intent weeks, months, or years after an application was written. But that doesn't mean it's too late to document the current state of affairs.

Here are four opportunities to add unit tests to existing code without undue risk or effort.

1. Troubleshooting code

Troubleshooting means reading through the source code, watching how the inputs are handled, tracing if/then/else statements, and following until the results are sent back. You can't avoid this work, but you can record the results for the next developer. Add the unit tests to show what you understand the code's behavior to be, and then run the unit tests to prove it.

And if you can't trace the behavior or just don't want to, you can use the unit test as an experiment. Take your best guess about the result, run the unit test, let it fail, and then update the test to reflect reality. Rather than an extra task you are saddled with, adding the unit tests becomes a tool in your troubleshooting arsenal.

2. Onboarding new developers

Teaching your codebase to someone new can be very similar to troubleshooting. Walking through the source, tracing the conditional paths, defining the edge cases, and describing the error handling are exercises common to both.

When teaching the codebase, take the opportunity to record your understanding in the form of unit tests. Even better, let the new developers write the unit tests as the code is explained. It gives them a chance to contribute code and makes them active participants in onboarding. Letting them ask probing questions will make them truly understand the code much more than they would if they were just listening to you lecture.

3. Updating libraries

Updating libraries is a never-ending maintenance task with modern software projects. The framework, the utilities, and even the programming language itself change so often that whole releases can be dedicated to nothing but making updates and testing them.

Because they are risky, repetitive, and usually add little to nothing that users see, library updates frequently get pushed down the road, until the project is forced to upgrade. It can be a vicious cycle: You want to make sure the upgrade doesn't break something, and the chance of that happening goes up the longer you go between upgrades.

Writing developer tests that involve the calls from your code to the libraries and frameworks you depend on can become a comforting safety net to keep the app's behavior consistent during upgrades. To the purist, that stretches the definition of "unit test," but these can still be quick tests using your unit test framework to document the code's behavior.

Why not document the behavior and make the next upgrade easier? If that lessens inertia toward taking the next minor update and the one after, it is a win all around. More incremental updates mean less risk in each one. You can be proactive and more likely to have the latest patches.

Another winning move is to document the library behaviors you rely on when your code interacts so that you aren't forced to read through change logs to find them. Build yourself a good set of tests, and you'll see the impact of upgrades right away in the context of your own code.

4. Changing behavior

Unit tests can be a great way to ensure that behavior doesn't change, but what if the goal is to change behavior? After all, even legacy code gets new functionality from time to time.

Again, unit tests can be a great guide—the first step in changing code behavior is understanding its current behavior. Write the tests, demonstrate the current behavior, and change the tests to reflect the desired behavior. Then you change the code so the new tests pass.

It's TDD on existing code. It forces the discussions of the changes upfront while the tests are being modified, and it keeps you from a rabbit hole of second-guessing and scope creep.

Clean up with unit tests

“Always leave the campground cleaner than you found it.”

That rule from the Boy Scouts of America has been adopted by the software industry as a maintenance best practice.

Whenever you are editing code, clean it up. That might mean refactoring or modernizing (and there are several ways to approach that), but it almost certainly starts with adding unit tests. Whether you are in the source code to troubleshoot, to teach a new developer how it works, doing updates, or adding new functionality, take some time to add unit tests.

You'll be cleaning the campground for those who come after you, which might be you on a day when you don't have the time or patience to unravel messy code.

Want to know more? Drop by for my talk, "How I Learned to Stop Worrying and Love Legacy Code," at the Agile + DevOps Virtual conference. The conference runs June 7-11, 2021. I will be speaking on June 9.

Keep learning

Read more articles about: App Dev & TestingTesting