fd Blog

Daniel Hilgarth on software development

MvvmCross: Enable Unit-testing

MvvmCross makes heavy internal use of Service Location and Singletons.
This results in necessary setup before being able to use it in a unit-testing scenario. ### Basic setup Many ViewModels in an MvvmCross application derive from MvxViewModel.
This class and/or its base classes rely on MvvmCross being initialized. In a MvvmCross application, this is automaitcally taken care of.
However, when trying to instantiate a ViewModel inside a unit test you will get a not so nice NullReferenceException. Some pocking around the source code shows that it tries to use a Singleton instance which is not initialized.

To fix this, you need to setup MvvmCross before the test. The code to do so is as follows:

public class MvvmCrossTestSetup
{
    private IMvxIoCProvider _ioc;

    protected IMvxIoCProvider Ioc
    {
        get { return _ioc; }
    }

    public void Setup()
    {
        ClearAll();
    }

    protected void ClearAll()
    {
        // fake set up of the IoC
        MvxSingleton.ClearAllSingletons();
        _ioc = MvxSimpleIoCContainer.Initialise();
        _ioc.RegisterSingleton(_ioc);
        _ioc.RegisterSingleton<IMvxTrace>(new TestTrace());
        RegisterAdditionalSingletons();
        InitialiseSingletonCache();
        InitialiseMvxSettings();
        MvxTrace.Initialize();
    }

    protected void InitialiseMvxSettings()
    {
        _ioc.RegisterSingleton<IMvxSettings>(new MvxSettings());
    }

    protected virtual void RegisterAdditionalSingletons()
    {
    }
    
    private static void InitialiseSingletonCache()
    {
        MvxSingletonCache.Initialise();
    }

    private class TestTrace : IMvxTrace
    {
        public void Trace(MvxTraceLevel level, string tag, Func<string> message)
        {
            Debug.WriteLine(tag + ":" + level + ":" + message());
        }

        public void Trace(MvxTraceLevel level, string tag, string message)
        {
            Debug.WriteLine(tag + ": " + level + ": " + message);
        }

        public void Trace(MvxTraceLevel level, string tag, string message, params object[] args)
        {
            Debug.WriteLine(tag + ": " + level + ": " + message, args);
        }
    }
}

Reference: MvvmCross repository

Testing Navigation between Views

Navigation in MvvmCross happens via calls to the method ShowViewModel which is declared on MvxNavigatingObject - a base class of our ViewModels.
In other words: Our ViewModels simply call a base class method for navigation, they don’t use some kind of service we could easily mock and inject into them.
Well, at least not directly:
It turns out that MvxNavigatingObject does use a service to perform the actual navigation. It indirectly gets a hold of this instance via a Singleton. Reconfiguring this Singleton allows us to provide our mock.
Reconfiguration of the Singleton is done by:

  1. Creating a class MockViewDispatcher that derives from MvxMainThreadDispatcher and implements IMvxViewDispatcher
  2. Registring an instance of it in the test setup

Implementing MockViewDispatcher

This class can be implemented differently, mainly depending on your style of testing. One implementation by Stuart Lodge, the author of MvvmCross, can be seen in the references.
Another implementation is the following, which simply delegates the calls to our “actual” mock:

public class MockMvxViewDispatcher : MvxMainThreadDispatcher, IMvxViewDispatcher
{
    private readonly IMvxViewDispatcher _decorated;

    public MockMvxViewDispatcher(IMvxViewDispatcher decorated)
    {
        _decorated = decorated;
    }

    public bool ChangePresentation(MvxPresentationHint hint)
    {
        return _decorated.ChangePresentation(hint);
    }

    public bool RequestMainThreadAction(Action action)
    {
        return _decorated.RequestMainThreadAction(action);
    }

    public bool ShowViewModel(MvxViewModelRequest request)
    {
        return _decorated.ShowViewModel(request);
    }
}

Registering the Singleton

It is important to know that instantiating our mock class performs the actual registration of itself as the Singleton! So, all that’s really necessary is to create an instance of that class:

new MockMvxViewDispatcher(viewDispatcherMock);

viewDispatcherMock is a mock of IMvxViewDispatcher created by your favorite mocking framework.

The sample code from Stuart also shows registration of the instance of our mock class in the Service Locator.
My sample tests pass without it, but there may be areas that require it, so I included it in my code as well.
The complete registration now looks like this:

var dispatcher = new MockMvxViewDispatcher(viewDispatcherMock);

Ioc.RegisterSingleton(dispatcher);
Ioc.RegisterSingleton<IMvxMainThreadDispatcher>(dispatcher);

Reference: MvxTest and MockMvxViewDispatcher.

Comments