How to make static legacy code testable
I had the opportunity to work in a team developing and improving a large legacy application. The project had some unit tests already but there was a long way to go in terms of increasing the test coverage.
Many internal services are implemented as a bunch of static methods like so:
1 | static class SeriousService |
And the usages of these static classes are all over the place like so:
1 | class SomeModule |
The challenge was to write a unit test which would test ExecuteOrder() without side effects in SeriousService.
A state-of-the-art application would insert the service through dependency injection into SomeModule. You could provide a mock for the service and you would be ready to write your unittest.
Unluckily, changing the caller or the signature of the callee was no option to me. The chance to introduce incompatibilities somewhere in the huge family of applications and tools was to high.
After some thinking I came up with the idea to extract the functionality out of the static service:
The “new” static service endpoint has now a private static reference to the service implementation and all public methods forward there calls to this implementation. The new service implementation is based on an extracted interface. Caller and callee are still unchanged (red rectangle). In code, it looks like so:
1 | interface ISeriousService |
Until here, all I have done is to insert another level of indirection. But this enables us to replace the real functionality of the service with a mock:
This is possible if the service “endpoint” gets a public method to replace its implementation:
1 | static class SeriousService |
Having all of this in place you can write your unit test for the module without the fear of any unforseen sideeffects:
1 | [TestFixture] |
There is one more thing left to be done. Obviously the real instance of SeriousServiceImpl is gone for good after executing the test. So if you want to be able to reuse the current AppDomain for another test you either need to recover it somehow or you have to replace it in every test. I have a nice solution for this as well but this article is already long enough.
So summing up, I introduced another level of indirection into my architecture. I used it to swap the implementation of a static service as was needed to write unit tests without side effects.