Featured Webinar: MISRA C++ 2023: Everything You Need to Know | Watch Now

Working With Legacy Code and 3 Steps to Update It

Headshot of Igor Kirilenko, Chief Product Officer at Parasoft
December 6, 2023
7 min read

Working with legacy code is an everyday reality. However, the gaps in knowledge about the code represent potential risks. Read on to understand how to mitigate these risks and update your legacy code.

When you’re dealing with legacy code, you need a sustainable way to manage change. Working with legacy code can be a barrier to Agile and DevOps, but you can conquer the challenge by leveraging appropriate technologies.

What Is Legacy Code?

Many people use the term “legacy code” to simply mean old code. But “old” and “legacy” mean different things to different people. Here, I’m using the definition of legacy code as any existing code that is no longer supported or updated, and the team has limited knowledge about it.

Knowledge about the code could be incomplete for several reasons, such as:

  • The team acquired a project from another part of the organization.
  • The original author left the team and took knowledge about the code with him or her.
  • The functionality delivered by the code is no longer a business priority and has remained unchanged, resulting in forgotten details about the code.

In any case, let’s be clear: legacy code is the rule, not the exception. Much of the software infrastructure in the world today runs on legacy code. The question is, then, how do we mitigate the risks associated with legacy code when we need to make a change? In this blog post, I’ll give you some solutions for working effectively with legacy code.

Legacy Code Is a Barrier to Agile and DevOps

The problem with legacy code isn’t its age — it’s that you don’t understand how changing it can affect existing functionality. The knowledge gaps associated with legacy code can become a barrier if you are transitioning to a new development methodology, such as Agile or DevOps.

Agile and DevOps have become the dominant methodologies for creating software because they help teams quickly iterate and release applications as soon as minimal marketable features are ready. Short and frequent development cycles are the hallmark of iterative development methodologies, but these approaches often don’t leave room for mitigating potentially problematic outcomes when you’re dealing with legacy code. Trying to rapidly iterate code you don’t understand is likely to introduce new issues.

Keeping up with changes in source code is much easier when starting new projects. For projects that have been around for a while, teams usually work with systems that involve legacy code. Developers may not know how the existing code base works but must still fix defects or extend functionality without introducing new problems. And even superficial or seemingly small changes can have a significant impact on the application.

Mitigating Technical Debt

The software development game is about constantly balancing software quality, time to market, and cost of development. In most cases, we make tradeoffs to achieve business goals based on what’s happening in the market. Over time, we rack up technical debt.

What Is Technical Debt?

Technical debt is the cost of mitigating the risk associated with implementing an imperfect solution to achieve your time-to-market or cost-of-development goal. For example, forgoing upgrades to a library because doing so would have delayed the release represents technical debt in the form of time it will take to update the library later.

In many cases, inherited legacy codebases are heavy with technical debt in the form of poor testability, low coverage, overly complex code, and so on. Technical debt can weigh down the application of newer software development practices because teams constantly face the question of whether to address the debt.

Should You Be Concerned About Technical Debt?

To put it in perspective, every application has technical debt, and many organizations can invest significant resources in paying it down without realizing any substantive benefits. At the end of the day, the decision to invest resources into paying off technical debt depends on which parts of your application you plan on changing. But you won’t know unless you start taking some additional steps. I’ll get into that momentarily.

Boosting Coverage on Legacy Code

When organizations inherit a legacy codebase to deal with, they often adopt a coverage policy that helps them create a baseline for new development. The legacy code is already in the field and supposedly working, so the focus is on ensuring the quality of new code. To comply with the coverage policy, many organizations drive up the coverage of the legacy code through the creation of many new unit tests. Low coverage drags down the overall metrics, which makes it difficult to accurately measure coverage for your new development. If you know that you’re working with legacy code that’s well covered, the overall project metrics can indicate if new development is moving in the right direction.

The underlying rationale for this strategy is sound, but the problem is that organizations don’t usually have time to create good-quality unit tests to cover those gaps. There should be a better way to handle legacy code.

Create Meaningful, Maintainable Java Tests

Use a tool that helps you rapidly create meaningful tests to cover your Java legacy code. Parasoft Jtest provides a point-and-click interface that gives developers an automatic test creation process based on the existing code. The resulting regression suite is meaningful, maintainable, and extensible. Customers report saving over 50% of their dev efforts to create tests for their legacy code.

The 3 Steps for Updating Your Legacy Code

Rather than trying to work on the macro level, create a baseline and narrow down the scope of your quality activities to the areas of code affected by your planned changes. After taking measurements to assess the scope and state of the code, you should create tests that capture current behavior so that the team can understand how the changes may affect existing functionality.

You can then leverage a range of technologies that help you collect analytics as you refactor legacy code and ensure that your investment in code changes improves the safety, security, and reliability of legacy systems.

1. Define Your Scope

Understanding how changes affect system behavior requires at least one data point. Begin by choosing a baseline build and start tracking metrics moving forward. Set your scope and look at three characteristics of the legacy code.

  • How many static analysis violations do you have and how severe are they? You need to understand how many potential defects are built into the code.
  • What is your current test coverage? Low coverage represents a potential risk associated with change.
  • How much cleanup will be necessary? Additional metrics, such as complexity, comments, and the like can provide perspective about the state of the software quality.

Parasoft provides a powerful analytics platform for capturing, correlating, and reporting code analysis violations, test results, coverage analysis, and other software quality data. The platform goes beyond static reporting. It also applies additional analysis to help you identify parts of the application affected by change.

You can identify a specific set of files or directories and scope coverage, static analysis violations, and metrics data to those specific resources. This information helps you create a baseline for areas of the codebase before making changes within those parts of the code.

2. Capture Behavior

Armed with an initial data point, the next step is to start capturing the current behavior of the system by creating tests. Building up a high-quality regression suite captures existing behavior, creating a safety net for making sure that changes don’t break functionality.

Parasoft Jtest is ideally suited to this task because it enables you to create a baseline of JUnit tests in bulk, including assertions, based on the existing code. Jtest also includes the ability to create tests that directly access private methods when the legacy code was not originally written with testability in mind.

It’s better to expand coverage with meaningful tests. Jtest can create unit tests just for the uncovered parts of the code, preventing redundant tests for code that already has some unit tests.

Parasoft Jtest can also be integrated with OpenAI or Azure OpenAI providers, which enables developers to further augment their tests through the input of user-crafted natural-language prompts that describe how test cases should be refactored. This provides flexible customization options for developers looking to quickly modify test cases in specific ways pertinent to their organization’s requirements.

You should strive for the highest level of coverage possible, but in most cases achieving 100% coverage on the entire codebase is not practical. Keep reading for an additional technique you can apply as a safety net to ensure coverage on modified code.
When you have good coverage from a functional perspective, you can start making changes and modifying the tests as you go.

3. Improve the Isolated Legacy Code

With the behavior of the system captured, you can start fixing violations, addressing PRs, or applying the changes you want to focus on with minimal risk of breaking existing functionality. Parasoft can help you manage the existing technical debt and put data, such as static analysis violations, into appropriate workflows where they can be easily reprioritized, suppressed, or resolved to improve the overall quality of the application. Changes from build to build should also be monitored as part of the ongoing process, to ensure that the software quality doesn’t take a turn for the worse.

The best time to address the technical debt in the legacy code is as you make changes. The reported data should be included in the overall statistical information about the project. The technical debt may not have an immediate impact on the application, but you should apply best practices for containing and managing it systematically. Refactoring legacy code any time you need to make changes helps you incrementally reduce the debt.

Ensuring Coverage on Modified Code

The process we’ve been discussing ensures that the scope of changes doesn’t negatively impact existing functionality, but you also need to make sure that the team follows good practices moving forward. Continuing to maintain a high level of coverage and writing or updating tests as the code evolves requires buy-in on a cultural level. This is why Parasoft developed technology that automatically notifies you when modified code—new or changed—fails to comply with the coverage policy.

By analyzing the changes between specified baseline builds, you can focus on and monitor the changes across the whole codebase to ensure nothing slips through the cracks. Achieving 100% coverage across the entire codebase might not be practical, but by monitoring the coverage of the modified code, the team can focus on the parts of the code that are actively being worked on and have confidence that all changes are tested.

In Conclusion: Working With Legacy Code

The world’s software runs on code that has been passed on from team to team. Dealing with legacy code is an everyday reality. The gaps in knowledge about the code represent potential risks as developers make changes to maintain or extend the functionality, and the processes and technologies described here should help you gain the confidence to take on just about any codebase thrust upon your teams.

Get Unit Testing Done Right: Tops Tips for Developers