All-In-One Scriptless Test Automation Solution!

Generic selectors
Exact matches only
Search in title
Search in content

Use Case Study

Code Migration & Testing: Refactoring legacy codes written in outdated languages such as AngularJS, VB.NET, Visual Basic, C, and/or .NET Framework

Overview

Why choose refactoring?

Our client is a financial services company in the US providing retirement planning services as a key offering. They had a legacy core system that had undergone many changes since 2000s in multiple applications – used for processing retirement plans and retirement savings related processes. The legacy application was written in Java 1.4 and ran on an un-supported application server built using legacy codes.

Java 1.4 had to be switched to a supported Java version and updated to a contemporary application server. The application team was worried about making changes without a safety net of tests as the codebase had grown over time and had complex dependencies, both with internal and external applications.

The decision makers responsible for system modernization therefore had two options to modernize their existing code base. Either undertake a complete rewrite, which is essentially building a whole new application, or put some serious effort into refactoring current code and configuration.

The choice to rewrite is a big one, and it requires massive investments into new skill and new technologies. Hence, the client opted for a refactoring strategy to make incremental changes in the internal structure of software, thereby making it easier to understand and cheaper to make modifications while keeping the core functionalities intact.

Our Solution: In-House Automation Testing Tools

Test Automation Tools: Our QA team uses a proprietary automation tool that integrates with Playwrite, an end-to-end testing software for web applications and Cypress for the applications Java code upgrade.

Static Analysis: The QA Team used this in-house automation tool to detect any potential issues introduced during the code refactoring. These tools can help identify unintended side effects.

Proficiency in Legacy Frameworks: Our QA and Test Engineers brought their hands-on experience in working with legacy frameworks such as VB.NET, Visual Basic, Visual FoxPro

Right Modernization Roadmap: We classified the requirement into part such as the follows and divided the testing and QA team accordingly:

  1. A team dedicated to legacy application codes that just require a change of interface of the legacy system – migrate to Java based interface
  2. A team dedicated to testing parts of the application that have been integrated with multiple new features
  3. A team dedicated to complete transitioning of hybrid legacy code of the application to the most updated version of Java

Challenge: Testing legacy code often requires making changes to the production code

Legacy systems typically lack the features and structures necessary to support modern testing practices. Here’s why changes in production code are sometimes necessary:

  1. Lack of Testability

Tightly Coupled Code: Legacy code often has tightly coupled components, making it difficult to isolate individual units for testing. To enable unit testing, developers may need to refactor the code to decouple these components, which involves changing the production code.

Difficulty in mocking dependencies: Dependencies are difficult to track in legacy systems, hence making it challenging to mock them in tests. Refactoring of code requires accepting dependencies through parameters or constructors that reflect changes in the production code.

  1. Absence of Automated Tests

Adding Test Hooks: Legacy code might not be designed with testing in mind, lacking hooks or interfaces that allow automated testing. Developers may need to add such hooks or refactor the code to make it testable, which necessitates modifications to the production code.

Testable Architectures: Modern testing relies on certain architectural patterns, such as Model-View-Controller (MVC) or Service-Oriented Architecture (SOA). Legacy code may need to be restructured or partially rewritten to adopt these patterns, requiring changes to the production code.

  1. Difficulty in Isolating and Mocking

Hard-Coded Dependencies: Legacy code often has hard-coded dependencies, such as database connections or file paths, making it difficult to test in isolation. To facilitate testing, these dependencies might need to be abstracted into interfaces or replaced with configurable options, which involves changes to the production code.

Use of Static Methods: Use of static methods in legacy code can create hidden dependencies that make testing difficult. Refactoring the code to minimize global state or to avoid static methods can improve testability, but it requires altering the production code.

  1. No Separation of Concerns

Single Responsibility Principle: Legacy code often violates the Single Responsibility Principle, with methods or classes performing multiple tasks. To make the code testable, developers might need to break down these methods or classes into smaller, single-responsibility units, which involves changes to the production code.

Extracting Methods or Classes: Large methods or classes in legacy code might need to be split into smaller, more manageable pieces to facilitate testing. Extracting methods or creating new classes to handle specific tasks requires modifying the production code.

  1. Enhancing Code Coverage

Unreachable Code: Some parts of the legacy code may be difficult or impossible to reach through the existing code paths, making it hard to test those areas. Developers might need to modify the code to expose these paths or make them more accessible for testing.

Introducing Logging and Monitoring: To better understand how legacy code behaves under different conditions, developers might add logging or monitoring capabilities. While these changes improve the ability to test and debug the code, they also alter the production code.

  1. Improving Performance and Stability

Performance Optimization: Legacy code might not perform well under test conditions, especially if it was written without considering modern performance standards. Optimizing the code to run efficiently in a test environment might involve changes to the production code.

Stabilizing the Codebase: Legacy code might contain fragile or unstable sections that cause tests to fail intermittently. Stabilizing these sections to make the codebase more reliable often requires changes to the production code.

  1. Compliance with Testing Frameworks

Adapting to Modern Frameworks: Modern testing frameworks require code to adhere to certain practices and patterns. Legacy code might need to be refactored to be compatible with these frameworks, necessitating changes to the production code.

Adding Annotations or Metadata: Some testing frameworks rely on annotations or metadata to identify testable components. Adding these to legacy code requires modifying the production code.

  1. Creating a Safety Net for Future Changes

Building a Test Suite: To create a safety net for future changes, developers might need to modify the legacy code to make it more testable, allowing them to build a comprehensive test suite. This ensures that future changes can be made with confidence, but it requires initial changes to the production code.

Incremental Refactoring: Refactoring legacy code incrementally, in a way that introduces tests along the way, involves changing the production code in small, controlled steps to gradually improve its testability.

How We Helped: We Ensured the Refactoring Does Not Change Production Code

Our refactoring strategy has improved the internal structure of the code without altering its external behavior. Here’s how we ensured that the behavior of production code remains unchanged during refactoring:

  1. Comprehensive Test Coverage

Automated Tests: Before starting refactoring, we ensured there is comprehensive test coverage, including unit tests, integration tests, and functional tests. These tests covered all critical paths and edge cases in the code.

Baseline Testing: We ran all tests to establish a baseline before refactoring. This ensured that the code behaves as expected before any changes are made.

Test-Driven Development (TDD): Wherever possible, we used TDD to write tests before making changes. This ensured that the refactored code passed the same tests as the original code.

  1. Refactoring in Small Steps

Incremental Changes: Make small, incremental changes rather than large, sweeping changes. This makes it easier to identify any issues that arise and ensures that the code remains stable throughout the process.

Continuous Testing: After each small change, run the full test suite to confirm that the behavior remains consistent. This continuous feedback loop helps catch issues early.

  1. Maintained Behavioral Consistency

Behavior-Preserving Transformations: Ensure that each refactoring step is a behavior-preserving transformation, meaning that it changes the structure or organization of the code without altering its functionality or outputs.

Refactoring Patterns: Use well-known refactoring patterns, such as extracting methods, renaming variables, or simplifying expressions, that are designed to be safe and behavior-preserving.

  1. Used Version Control

Branching: Use of version control to create a separate branch for refactoring was an all-encompassing strategy. It isolated the changes and allowed reverting to the original code, if necessary, without affecting the production code.

Committed Often: Committing changes frequently, along with passing test results, created a clear history of what was changed and ensured that each step maintains the code’s behavior.

  1. Peer Review and Pair Programming

Code Reviews: We had additional developers to review refactoring changes to ensure there are no unintended behavior changes. Peer reviews helped catch issues that might be missed by automated tests.

Pair Programming: We engaged in pair programming during refactoring. Having additional developer to work alongside testers helped ensure that changes remain behaviorally consistent.

  1. Document Refactoring Changes

Refactoring Logs: Our QA Team kept a log of all refactoring activities, including what was changed and why. This helped track the rationale behind the changes and makes it easier to review and verify that the code’s behavior has not been altered.

Update Documentation: Ensured that any changes to the code’s structure are reflected in the documentation, particularly if the refactoring improves or alters how the code is understood.

  1. Use Feature Flags (if applicable)

Feature Flags: Whenever refactoring involved changing or introducing a new functionality, we used feature flags to toggle the new code on or off. This allows us to safely deploy the refactored code and test it in production without exposing it to all users.

Staged Rollout: We gradually rolled out the refactored code using feature flags, starting with a small subset of users or environments. Monitor for any issues before fully enabling the changes.

  1. Refactoring with a Safety Net

Canary Releases: Our QA team deployed the refactored code to a small segment of the production environment (canary release) to observe its behavior in a controlled manner. It helped us identify any unexpected issues before the full deployment.

Monitored Production Metrics: Our QA and Test Engineers an eye on production metrics (e.g., performance, error rates) during and after the refactoring process to quickly identify and address any anomalies.

  1. Rollback Plan

Prepare Rollback Plan: We had set up a pre-meditated rollback plan in place before refactoring. So that if at all anything goes wrong, we can quickly revert to the previous, stable version of the code.

Backup and Restore: Our QA and Test Engineers had ready mechanisms in place to restore the previous version of the code and data if necessary.

Impact

  • Zero code breakage when migrating to Java 1.4
  • 90% savings in time spent on testing new release
  • 50% reduced time spent on troubleshooting codes where issues appear
  • 100% test coverage in systems that cannot be interfaced with directly

 

Download More Case Studies

Get inspired by some real-world examples of complex data migration and modernization undertaken by our cloud experts for highly regulated industries.

Contact Your Solutions Consultant!

India Job Inquiry / Request Form

US Job Inquiry / Request Form

Apply for Job