In the relentless march of technological progress, software systems designed over a decade ago often become formidable fortresses of “untestable” code. For many investigative developers, this presents a daily challenge, particularly when confronted with architectures heavily reliant on static methods and singletons to access core platform functionalities. The question isn’t just *how* to test such code, but *what* it reveals about our design philosophies.
Consider the all-too-common scenario: a plugin architecture where the core lifecycle method, `start()`, is deeply entangled with global singleton instances, defying conventional testing approaches.
@Override
public boolean start() {
var aService = AService.getInstance();
var anotherService = AnotherService.getInstance();
// Do something with the services
var result = ...;
return result;
}
For years, developers grappled with this. The Mockito framework, a titan in the Java mocking ecosystem, initially resisted the capability to mock static methods, pushing many towards alternatives like PowerMock. This stance shifted in 2020 with Mockito’s 3.4.0 release, which finally introduced static method mocking. But does this newfound power truly liberate us, or does it merely offer a more convenient crutch for lingering design issues?
The Dilemma of Convenience: Mocking as a Design Smokescreen?
The very availability of static method mocking sparks a crucial debate. Is it a pragmatic tool for navigating the realities of legacy systems, or does it inadvertently obscure deeper design flaws? Critics argue that having to mock static methods is a glaring symptom of a poorly architected system. While PowerMock, in its early days, might have provided a temporary scaffolding to allow testing *before* a necessary redesign, the seamless integration of static mocking into mainstream frameworks like Mockito could potentially reduce the impetus to refactor and address the underlying design smells. When faced with an existing system where design changes are simply not an option, however, practicality often trumps purity.
Unveiling the “Strangely Straightforward” Solution
For those trapped in the legacy quagmire, unable to overhaul core architectural decisions, a surprisingly simple solution emerges—a wrapper method. This technique allows developers to isolate the testable logic from the static dependency resolution, providing a clean seam for unit testing without altering the external behavior of the original method.
@VisibleForTesting //1
boolean start(AService aService, AnotherService anotherService) { //2
// Do something with the services
var result = ...;
return result;
}
@Override
public boolean start() {
var aService = AService.getInstance();
var anotherService = AnotherService.getInstance();
return start(aService, anotherService); //3
}
- The new method, originally envisioned as
private, is made package-visible to facilitate testing. The@VisibleForTestingannotation serves as critical documentation, signaling its purpose to future developers. - Crucially, this new `start` method accepts the dependencies (`AService`, `AnotherService`) as parameters. This is the key to testability, as these parameters can now be easily mocked during tests.
- The original `@Override` method simply becomes a thin delegator, responsible only for fetching the actual singleton instances and passing them to the now-testable wrapper method.
This approach offers a clear path to testing legacy code that wasn’t built with modern Dependency Injection principles in mind. It’s a testament to the ingenuity often required when working within the constraints of established, deeply entrenched systems.
While elegant in its simplicity and highly effective for tackling the “untestable,” this method raises a pertinent question for the future of software development: Do such pragmatic workarounds ultimately perpetuate or mitigate the challenges of technical debt in our constantly evolving digital landscape?




