Concurrent Unit Tests with Service Locator
My talk at Microsoft Summit created a nice discussion with some of the participants about writing isolated unit tests when using the Service Locator.
It started from the part where I was showing how the AppBoot helps in dependencies management by enforcing consistency on how the Dependency Injection is done. With the AppBoot we make sure that Dependency Injection is used and that it is done only through the constructor. The question that started the discussion was:
Does this mean that I will have constructor parameters for everything? Including utility services like
ILog
? If so, it means that I will pollute the constructors with these details and…. things may get over complicated
My answer was that for logging or other similar utilities we could make static helpers that make them easier to be called. Such a helper would wrap a ServiceLocator
, so we do not make a strong dependency on the logging implementation or library. Something like this:
public static class Logger
{
public static void Error(string headline, string message)
{
ILogSrv log = ServiceLocator.Current.GetInstance<ILogSrv>();
log.WriteTrace(new Trace(headline, message, Severity.Error));
}
public static void Warning(string headline, string message)
{
ILogSrv log = ServiceLocator.Current.GetInstance<ILogSrv>();
…
}
public static void Trace(string functionName, string message)
{
ILogSrv log = ServiceLocator.Current.GetInstance<ILogSrv>();
…
}
public static void Debug(string message, object[] variables)
{
ILogSrv log = ServiceLocator.Current.GetInstance<ILogSrv>();
…
}
}
This makes my class code to depend on some static functions (Logger.Error()
), but that seems a good compromise as long as the underneath things remain as simple as in the above snippet.
Now, if we are to write some unit tests in isolation, we would like to use a stub for the ILogSrv
interface, and we can do that by making a setup like this:
[TestClass]
public class UnitTests
{
private Mock<IServiceLocator> slStub;
[TestInitialize]
public void TestInitialize()
{
slStub = new Mock<IServiceLocator>();
ServiceLocator.SetLocatorProvider(() => slStub.Object);
}
[TestMethod]
public void PlaceNewOrder_FromPriorityCustomer_AddedOnTopOfTheQueue()
{
Mock<ILogSrv> dummyLog = new Mock<ILogSrv>();
slStub.Setup(l => l.GetInstance<ILogSrv>()).Returns(dummyLog.Object);
…
}
…
}
This code configures the ServiceLocator.Current
to return an instance which gives the dummy ILogSrv
when needed. Therefore, the production code will use a dummy ILogSrv
, which probably does nothing on WriteTrace()
.
For logging this may be just fine. It is unlikely that we would need different stub configurations for ILogSrv
in different tests. However, things may not be as easy as this, for other services that are taken through the ServiceLocator.Current
. We might want different stubs for different test scenarios. Something like this:
// — production code —
public class UnderTest
{
public bool IsOdd()
{
IService service = ServiceLocator.Current.GetInstance<IService>();
int number = service.Foo();
return number%2 == 1;
}
}
// — test code —
private Mock<IServiceLocator> slStub = new Mock<IServiceLocator>();
ServiceLocator.SetLocatorProvider(() => slStub.Object);
[TestMethod]
public void IsOdd_ServiceReturns5_True()
{
Mock<IService> stub = new Mock<IService>();
stub.Setup(m => m.Foo()).Returns(5);
slStub.Setup(sl => sl.GetInstance<IService>()).Returns(stub);
…
}
[TestMethod]
public void IsOdd_ServiceReturns4_False()
{
Mock<IService> stub = new Mock<IService>();
stub.Setup(m => m.Foo()).Returns(4);
slStub.Setup(sl => sl.GetInstance<IService>()).Returns(stub);
…
}
Because our production code depends on statics (uses ServiceLocator.Current
to take its instance), when these tests are ran in parallel, we will run into troubles. Think of the following scenario: Test1
sets up the slStub
to return its setup for the IService
stub. Then, on a different thread Test2
overwrites this setup and runs. After that, when the code exercised by Test1
gets the IService
instance through the static ServiceLocator.Current
it will receive the Test2
setup, therefore the surprising failure.
By default MS Test or VS Test will run tests from different test classes in parallel, so if we have more test classes which do different setups using the ServiceLocator.SetLocatorProvider()
, we will run into the nasty situation that sometimes our tests fail on the CI server or on our machine.
So, what should we do?
One option is to avoid the dependencies to the statics and to get the service locator through Dependency Injection through constructor. This would make the above example like below:
// — production code —
public class UnderTest
{
private IServiceLocator sl;
public UnderTest()
{
sl = ServiceLocator.Current;
}
public UnderTest(IServiceLocator serviceLocator)
{
this.sl = serviceLocator;
}
public bool IsOdd()
{
IService service = sl.GetInstance<IService>();
int number = service.Foo();
return number%2 == 1;
}
}
// — test code —
[TestMethod]
public void IsOdd_ServiceReturns5_True()
{
Mock<IService> stub = new Mock<IService>();
stub.Setup(m => m.Foo()).Returns(5);
Mock<IServiceLocator> slStub = new Mock<IServiceLocator>();
slStub.Setup(sl => sl.GetInstance<IService>()).Returns(stub);
var target = new UnderTest(slStub.Object);
…
}
[TestMethod]
public void IsOdd_ServiceReturns4_False()
{
Mock<IService> stub = new Mock<IService>();
stub.Setup(m => m.Foo()).Returns(4);
Mock<IServiceLocator> slStub = new Mock<IServiceLocator>();
slStub.Setup(sl => sl.GetInstance<IService>()).Returns(stub);
var target = new UnderTest(slStub.Object);
…
}
This would be a good solution and I favour it in most of the cases. Sometimes I add, as in the above snippet, a constructor without parameters that is used in the production code and one which receives the ServiceLocator
as a parameter for my unit tests code.
The other option, which is the answer to the question at the start of the post, looks a bit more magical :). It fits the cases when we need and want to keep the simplicity the static caller brings. Here, we keep the production code as is and we make the unit tests to safely run in parallel. We can do this by creating one stub of the IServiceLocator
for each thread and record it on a thread static field. We can do it with a ServiceLocatorDoubleStorage class that wraps the thread static field and gives the tests a clean way to setup and access it.
public static class ServiceLocatorDoubleStorage
{
[ThreadStatic]
private static IServiceLocator current;
public static IServiceLocator Current
{
get { return current; }
}
public static void SetInstance(IServiceLocator sl)
{
current = sl;
}
public static void Cleanup()
{
SetInstance(null);
}
}
Now, the unit tests will use the ServiceLocatorDoubleStorage.SetInstance()
instead of the ServiceLocator.SetLocatorProvider()
. So the test code from the above sample transforms into:
[TestClass]
public class UnitTest
{
[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
// the production code will get it through
// ServiceLocator.Current, so this is needed
ServiceLocator.SetLocatorProvider(
() => ServiceLocatorDoubleStorage.Current);
}
private Mock<IServiceLocator> slStub;
[TestInitialize]
public void TestInitialize()
{
slStub = new Mock<IServiceLocator>();
ServiceLocatorDoubleStorage.SetInstance(slStub.Object);
}
[TestMethod]
public void IsOdd_ServiceReturns5_True()
{
Mock<IService> stub = new Mock<IService>();
stub.Setup(m => m.Foo()).Returns(5);
slStub.Setup(sl => sl.GetInstance<IService>()).Returns(stub);
…
}
…
}
With this, each time a new thread is used by the testing framework, the test on it will first set its own stub of the ServiceLocator
and then it will run. This makes that the ServiceLocator
stubs even if they are static resources, not to be shared among different tests on different threads. On the code samples from my Code Design Training, on github here, you can find a fully functional example that shows how this can be used and how it runs in parallel.
To conclude, I would say that Dependency Injection and Service Locator should be used together. I strongly push towards using Dependency Injection in most of the cases because it makes the dependencies clearer and easier to manage, but definitely there are cases where Service Locator is needed or makes more sense. In both cases writing isolated unit tests should be easy and may be a good check of our design and dependencies.