Unit testing with Xamarin.Forms' DependencyService
Xamarin.Forms ships with a built-in Service Locator, called DependencyService, which allows us to register and resolve dependencies. Typically, we use this to enable accessing and invoking platform specific logic in our shared code. The DependencyService
is very convenient and easy to use, but it does come with a limitation. Because the DependencyService
is part of the Xamarin.Forms framework, it expects that the framework has been initialized prior to using it. This presents a problem when we're trying to unit test some code that uses the DependencyService
.
Consider a typical cross platform view model class :
public class MainViewModel
{
public string Data { get; set; }
public void LoadData()
{
var database = DependencyService.Get<ISQLite>();
// Use the database
}
}
If we wanted to write a unit test for this, it might look like this :
[TestFixture]
public class TestMainViewModel
{
[OneTimeSetUp]
public void Setup()
{
// Use the testable stub for unit tests
DependencyService.Register<ISQLite>(new MyFakeSqliteImplementation());
}
[Test]
public void ViewModelShouldLoadData()
{
var vm = new MainViewModel();
vm.LoadData();
Assert.AreNotEqual(string.Empty, vm.Data);
}
}
Unfortunately, if we try to access the DependencyService
outside of an iOS/Android/Windows host project, we will receive an exception
You MUST call Xamarin.Forms.Init()
In our unit test, we haven't called Xamarin.Forms.Init()
anywhere, and we can't call that method outside of a host. In order to properly unit test the application, we are going to have to make some architectural changes.
Solution #1
The first possible solution would be to move away from the Service Locator pattern completely, and instead architect our app using Dependency Injection. There are many benefits to Dependency Injection over Service Locators, but this usually comes down to developer preference. There are many DI containers that work with Xamarin and I encourgage you to try them out.
Solution #2
If we really want to continue using the Service Locator pattern, but we also want to unit test our code, we will have to abstract Xamarin.Forms' implementation of the pattern.
To begin with, we need to create a Service Locator interface in our shared code project. Notice that this simply provides a way for our shared code to request an object based on a generic key. There is no reference or dependency to Xamarin.Forms at all.
public interface IDependencyService
{
T Get<T>() where T : class;
}
Next, we will change our MainViewModel
to take in an implementation of the IDependencyService
as a parameter. There are now two constructors for the MainViewModel
. In the normal case of running our app, we will instantiate and use a new DependencyServiceWrapper()
object (see below), which will continue to use Xamarin.Forms' DependencyService
object internally. The second constructor allows us to pass in any object that implements the IDependencyService
interface, including mocking or stubbing it out for unit testing purposes.
public class MainViewModel
{
private readonly IDependencyService _dependencyService;
public MainViewModel() : this(new DependencyServiceWrapper())
{
}
public MainViewModel(IDependencyService dependencyService)
{
_dependencyService = dependencyService;
}
public string Data { get; set; }
public void LoadData()
{
var database = _dependencyService.Get<ISQLite>();
// Use the database
}
}
The DependencyServiceWrapper
class will simply delegate its calls to Xamarin.Forms' built-in DependencyService
, giving us the same behavior as before while running the app on a host.
public class DependencyServiceWrapper : IDependencyService
{
public T Get<T> () where T : class
{
// The wrapper will simply pass everything through to the real Xamarin.Forms DependencyService class when not unit testing
return DependencyService.Get<T> ();
}
}
While unit testing, we can create a simple implementation of the interface to use. In this case, we're just going to use a Dictionary<Type, object>
to store the implementations. Even though the IDependencyService
interface only defined a Get()
method, the stub implementation also provides a Register()
method. Using the stub means that we never use the DependencyService
class, and therefore never have to call Xamarin.Forms.Init()
during a unit test.
public class DependencyServiceStub : IDependencyService
{
privet readonly Dictionary<Type, object> registeredServices = new Dictionary<Type, object>();
public void Register<T>(object impl)
{
this.registeredServices[typeof(T)] = impl;
}
public T Get<T> () where T:class
{
return (T)registeredServices[typeof(T)];
}
}
Finally, we can update our unit test to use the new DependencyServiceStub
.
[TestFixture]
public class TestMainViewModel
{
IDependencyService _dependencyService;
[OneTimeSetUp]
public void Setup()
{
_dependencyService = new DependencyServiceStub ();
// Use the testable stub for unit tests
_dependencyService.Register<ISQLite>(new MyFakeSqliteImplementation());
}
[Test]
public void ViewModelShouldLoadData()
{
var vm = new MainViewModel(_dependencyService);
vm.LoadData();
Assert.AreNotEqual(string.Empty, vm.Data);
}
}