Synergex at 50: Enterprise Computing on the Move (1980–1999)
July 13, 2026
From the Frontlines of Modernization: Customer Sessions at This Year’s Synergy DevPartner Conference
August 12, 2026Automated testing of new code tends to be relatively straightforward because it’s often written with testing in mind from the start. Legacy code, on the other hand, is rarely so accommodating. Over time, language constructs change and design patterns are superseded. This friction makes adding or updating automated tests (particularly unit tests) difficult because the code itself becomes a roadblock. The SDI product contains a significant amount of legacy code, and we’ve had to contend with these challenges while continuing to deliver new features and bug fixes. We haven’t found a silver bullet for testing legacy code, but we’re happy to share a set of practical, incremental improvements that you can apply when working with your own legacy codebases to reduce risk and make it easier to work with in the future.
Legacy code isn’t just code written 20+ years ago. Even code written today can quickly become legacy if it relies on outdated patterns, tightly coupled components, or hidden assumptions that make it hard to change safely. The SDI team deals with a mix of these issues due to both Visual Studio extensibility requirements and our own historical design decisions.
As an external component, Visual Studio dictates certain requirements that we can’t change. In these areas, our options are limited. We use Moq where possible (more on that later), and we rely heavily on clear code documentation to make these components easier to reason about for future development. Occasionally, a Visual Studio API update gives us an opportunity to revisit and improve our implementation for a feature or system, but these changes can be large undertakings and must be approached carefully.
The design of the SDI product internals, however, is something we can influence and that’s where most of our progress has come from.
One recurring pain point has been large routines. Large routines often require substantial setup just to exercise a single test condition, or they contain complex logic that’s difficult to reason about and validate. When possible, we simplify logic through early returns on error conditions or by clarifying control flow, which can reduce the amount of setup required. If a method can be broken down into smaller, meaningful pieces, we consider doing so, provided those pieces represent real operations and not abstractions created solely to “clean up” the code. Smaller, more atomic operations are generally easier to test and reason about than a single, monolithic routine.
Another challenge has been poor service dependency management. Some services have historically depended directly on concrete implementations or even created their own dependencies internally. Those dependencies, in turn, often had dependencies of their own. The result was a cascading setup cost that made unit testing impractical. Our approach to this problem has been to refactor dependencies to interfaces instead of concrete types, rely on dependency injection to control how services receive those dependencies, and use Moq to dynamically define dependency behavior for individual test scenarios.
For those unfamiliar with dependency injection, it’s a design pattern where a class receives its dependencies from an external source rather than creating or locating them itself. In practice, this often means dependencies are passed through a constructor, though other techniques exist. By moving dependency creation outside of a service, we eliminate hidden initialization requirements. This makes the code easier to understand, easier to reason about, and significantly easier to unit test.
Moq complements dependency injection on the testing side. Moq is a NuGet package for .NET and .NET Framework that allows us to create mock implementations of interfaces at runtime. Instead of building full, real implementations just to satisfy a test, we define only the behavior needed for the specific scenario we’re validating. Together, dependency injection and Moq allow us to isolate services and test them independently, which has opened the door to unit testing areas of the product that were previously tested only through the UI.
While it would be ideal to make these improvements across the entire codebase at once, we have to balance immediate value (resolving active issues and delivering new features) with long-term value from an improved and better-tested implementation. Our solution has been to make incremental improvements as we work in a given area of the product. This allows us to deliver both in parallel, while also giving us the opportunity to evaluate which design patterns truly improve the code and which ones risk becoming the next generation of legacy code.
Much of this article has focused on unit testing because that’s where the SDI team is currently investing most of its testing effort. Before integrating Moq into our testing toolkit, we relied far more on automated UI tests. This was a natural starting point: automated UI tests closely mirror manual testing and allow us to validate the product from the user’s perspective. Even today, they’ve helped us uncover issues that unit tests alone would have missed. However, UI tests come with trade-offs. They tend to be slower, more brittle, and harder to maintain as the product evolves. Over time, we’ve shifted our preference toward unit tests, which provide faster feedback and are less sensitive to unrelated changes. We still write UI tests when necessary, but typically only in scenarios where refactoring for unit testing is impractical or impossible.
Improving automated testing in a legacy codebase isn’t about rewriting everything or chasing perfection. For the SDI team, it’s been about making targeted, incremental improvements over time. The result is cleaner code, improved testability, and a reduced risk of regressions with each release. Legacy code doesn’t have to hold us back. With the right practices, even legacy systems can evolve safely and continue delivering value.