Unit Testing Navigation in MvvmCross

June 16th 2014 MvvmCross Unit Testing

The best resource on the subject of testing navigation in MvvmCross view models that I managed to find, was Slodge's blog post from almost two years ago. While it still contains useful guidance for today, there have been changes in the framework, which prevent direct usage of included sample code. After I got it to work in my own project I decided to publish a more up-to-date set of instructions here.

Apart from the already mentioned blog post, the best starting point is the Testing article in MvvmCross Wiki on GitHub. Although it doesn't explicitly focus on testing navigation, it still provides all the plumbing code, required to make it work. The core of it is MockDispatcher implementation of IMvxDispatcher and IMvxMainThreadDispatcher interfaces:

public class MockDispatcher : MvxMainThreadDispatcher, IMvxViewDispatcher
{
    public readonly List<MvxViewModelRequest> Requests = 
        new List<MvxViewModelRequest>();
    public readonly List<MvxPresentationHint> Hints = 
        new List<MvxPresentationHint>();

    public bool RequestMainThreadAction(Action action)
    {
        action();
        return true;
    }

    public bool ShowViewModel(MvxViewModelRequest request)
    {
        Requests.Add(request);
        return true;
    }

    public bool ChangePresentation(MvxPresentationHint hint)
    {
        Hints.Add(hint);
        return true;
    }
}

To use it in place of the default implementation, it must be registered before the tests are run. I suggest you include this initialization in a unit test base class, which you can derive from MvxIoSupportingTest class (distributed with MvvmCross.HotTuna.Tests NuGet package). This way you don't have to worry about it in each test class. You can just derive all your view model test classes from this one and everything will already be initialized. Here's my implementation of it (using MsTest unit testing framework):

public class ViewModelTestsBase : MvxIoCSupportingTest
{
    protected MockDispatcher MockDispatcher;
    protected override void AdditionalSetup()
    {
        base.AdditionalSetup();
        MockDispatcher = new MockDispatcher();
        Ioc.RegisterSingleton<IMvxViewDispatcher>(MockDispatcher);
        Ioc.RegisterSingleton<IMvxMainThreadDispatcher>(MockDispatcher);
        // required only when passing parameters
        Ioc.RegisterSingleton<IMvxStringToTypeParser>(new MvxStringToTypeParser());
    }

    [TestInitialize]
    public void TestInit()
    {
        Setup();
    }
}

Notice how I exposed the MockDispatcher instance as a protected field to make it available in derived test classes. This makes it possible for the tests to check the navigation calls that have been made by them. It is also important that this setup is done before each test to clear records of any calls triggered by other tests.

If you're not familiar with navigation in MvvmCross, you can learn about it from a great article in MvvmCross Wiki. In this post I'm only going to focus on testing three typical navigation scenarios.

For simple navigation without any parameters being passed, you just need to check the view model type included in the only request that has been made:

[TestMethod]
public void SimpleNavigationTest()
{
    var viewModel = new StartingViewModel();
    viewModel.NavigationCommand.Execute(null);

    Assert.AreEqual(1, MockDispatcher.Requests.Count);
    Assert.AreEqual(typeof(DestinationViewModel), 
        MockDispatcher.Requests[0].ViewModelType);
}

When the navigation requires parameters to be passed along, they need to be verified as well. They are included in the request as a collection of string key-value pairs:

[TestMethod]
public void NavigationWithParametersTest()
{
    var viewModel = new StartingViewModel();
    ViewModel.SelectedItem = new Item { Id = 42 };
    viewModel.NavigationCommand.Execute(null);

    Assert.AreEqual(1, MockDispatcher.Requests.Count);
    Assert.AreEqual(typeof(DestinationViewModel), 
        MockDispatcher.Requests[0].ViewModelType);
    Assert.AreEqual(1, MockDispatcher.Requests[0].ParameterValues.Count);
    Assert.AreEqual("42", MockDispatcher.Requests[0].ParameterValues["Id"]);
}

Navigating back is implemented in MvvmCross using the semantics of close operation, therefore such calls are logged as presentation hints, not requests:

[TestMethod]
public void NavigationBackTest()
{
    var viewModel = new StartingViewModel();
    viewModel.BackCommand.Execute(null);

    Assert.AreEqual(1, MockDispatcher.Hints.Count);
    Assert.AreEqual(typeof(MvxClosePresentationHint), 
        MockDispatcher.Hints[0].GetType());
}

That's all there is to it. Really simple, once you know how to do it.

Copyright
Creative Commons License