I’m trying to make a very large, very legacy project testable.
We have a number of statically available services that most of our code uses. The problem is that these are hard to mock. They used to be singletons. Now they are pseudo-singletons — same static interface but the functions delegate to an instance object that can be switched out. Like this:
class ServiceEveryoneNeeds
{
public static IImplementation _implementation = new RealImplementation();
public IEnumerable<FooBar> GetAllTheThings() { return _implementation.GetAllTheThings(); }
}
Now in my unit test:
void MyTest()
{
ServiceEveryoneNeeds._implementation = new MockImplementation();
}
So far, so good. In prod, we only need the one implementation. But tests run in parallel and might need different mocks, so I did this:
class Dependencies
{
//set this in prod to the real impl
public static IImplementation _realImplementation;
//unit tests set these
[ThreadStatic]
public static IImplementation _mock;
public static IImplementation TheImplementation
{ get {return _realImplementation ?? _mock; } }
public static void Cleanup() { _mock = null; }
}
And then:
class ServiceEveryoneNeeds
{
static IImplementation GetImpl() { return Dependencies.TheImplementation; }
public static IEnumerable<FooBar> GetAllTheThings() {return GetImpl().GetAllTheThings(); }
}
//and
void MyTest()
{
Dependencies._mock = new BestMockEver();
//test
Dependencies.Cleanup();
}
We took this route because it’s a massive project to constructor inject these services into every class that needs them. At the same time, these are universal services within our codebase that most functions depend on.
I understand that this pattern is bad in the sense that it hides dependencies, as opposed to constructor injection which makes dependencies explicit.
However the benefits are:
– we can start unit testing immediately, vs doing a 3 month refactor and then unit testing.
– we still have globals, but this appears to be strictly better than where we were.
While our dependencies are still implicit, I would argue that this approach is strictly better than what we had. Aside from the hidden dependencies, is this worse in some way than using a proper DI container? What problems will I run into?
Its a service locator which is bad. But you already know that. If your code base is that massive, why not start a partial migration? Register the singleton instances with the container and start constructor injecting them whenever you touch a class in your code. Then you can leave most parts in a (hopefully) working condition and get the benefits of DI everywhere else.
Ideally the parts without DI should shrink over time. And you can start testing right away.