Tuesday, October 30, 2012

Testing ViewModels in MvvmCross

One question I've been meaning to answer for a while is how I setup my unit tests so that I can test viewmodel behaviour.

This is actually very simple to do. There are just a couple of things to put in place to cope with the singleton nature of MvvmCross IoC

Also, please note that if you want to test on MonoTouch then because there are dynamic code restrictions, then you cannot use Dynamic Mocks (like Moq)

*1. First in the test setup, I clear the IoC system using code like:

    private IMvxServiceProviderRegistry _ioc;

    [SetUp]
    public void SetUp()
    {
        // fake set up of the IoC
        MvxOpenNetCfContainer.ClearAllSingletons();
        MvxOpenNetCfServiceProviderSetup.Initialize();
        _ioc = MvxServiceProvider.Instance;
        _ioc.RegisterServiceInstance(new MvxDebugTrace());
        MvxTrace.Initialize();
    }

*2. Then to test general navigation I use a couple of Mocks like:

public class MockMvxViewDispatcherProvider : IMvxViewDispatcherProvider
{
    #region IMvxViewDispatcherProvider implementation

    public IMvxViewDispatcher Dispatcher { get; set; }

    #endregion
}

public class MockMvxViewDispatcher : IMvxViewDispatcher
{
    public List CloseRequests = new List();
    public List NavigateRequests = new List();

    public MockMvxViewDispatcher()
    {
    }

    #region IMvxViewDispatcher implementation

    public bool RequestNavigate(MvxShowViewModelRequest request)
    {
        NavigateRequests.Add(request);
        return true;
    }

    public bool RequestClose(IMvxViewModel whichViewModel)
    {
        CloseRequests.Add(whichViewModel);
        return true;
    }

    public bool RequestRemoveBackStep()
    {
        throw new NotImplementedException();
    }

    #endregion

    #region IMvxMainThreadDispatcher implementation

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

    #endregion
}

The main thing these Mocks appear to do is to catch and store the navigation requests.

IMPORTANT NOTE: however, please do note the IMvxMainThreadDispatcher code - this service is essential to much of the operation of MvvmCross - lots of things in MvvmCross rely on this service being available because they need to get their execution across to some "UI thread" - e.g. PropertyChanged will silently fail to fire if the service is missing!

*3. To then test navigation I use tests like:

    [Test]
    public void ExecutingTheLoginCommandNavigatesToTheLoginViewModel()
    {
        var mockNavigation = new MockMvxViewDispatcher();
        var mockNavigationProvider = new MockMvxViewDispatcherProvider();
        mockNavigationProvider.Dispatcher = mockNavigation;
        _ioc.RegisterServiceInstance(mockNavigationProvider);

        var homeViewModel = new HomeViewModel();
        homeViewModel.LoginCommand.Execute(null);

        Assert.That(mockNavigation.NavigateRequests.Count == 1);
        Assert.That(mockNavigation.NavigateRequests.First().ViewModelType == typeof(LoginViewModel));
    }

    [Test]
    public void ExecutingTheCloseCommandClosesTheViewModel()
    {
        var mockNavigation = new MockMvxViewDispatcher();
        var mockNavigationProvider = new MockMvxViewDispatcherProvider();
        mockNavigationProvider.Dispatcher = mockNavigation;
        _ioc.RegisterServiceInstance(mockNavigationProvider);

        var loginViewModel = new LoginViewModel();
        loginViewModel.CloseCommand.Execute(null);

        Assert.That(mockNavigation.CloseRequests.Count == 1);
        Assert.That(mockNavigation.CloseRequests.First() == viewModel);
    }

*4. And to test ViewModel property changes I just use simple code like:

    [Test]
    public void ExecutingTheAddCommandIncrementsTheCounter()
    {
        var mockNavigation = new MockMvxViewDispatcher();
        var mockNavigationProvider = new MockMvxViewDispatcherProvider();
        mockNavigationProvider.Dispatcher = mockNavigation;
        _ioc.RegisterServiceInstance(mockNavigationProvider);

        var tipViewModel = new TipViewModel();
        Assert.That(tipViewModel.Counter == 0);
        var notifications = new List();
        tipViewModel.PropertyChanged += (s, e) =>
            {
                notifications.Add(e.PropertyName);
            };

        tipViewModel.CloseCommand.Execute(null);

        Assert.That(mockNavigation.CloseRequests.Count == 0);
        Assert.That(mockNavigation.NavigateRequests.Count == 0);
        Assert.That(notifications.Count == 1);
        Assert.That(notifications[0] == "Counter");
        Assert.That(tipViewModel.Counter == 1);
    }

*5. And that's it... these tests work cross-platform - including in the lovely NUnit tool from Xamarin for MonoTouch

2 comments:

  1. Hello,

    I'm tryong to use the code in this post to test some ViewModels developed using the lastest version of vnext but seems that MvxOpenNetCfServiceProviderSetup class is not present in the sources anymore. Am I missing something ? Can you point me to a detailed configuration tutorial/instruction maybe also inclding the setup for a ViewModel that uses the SQLite plugin too?

    ReplyDelete
  2. For an update, try something like: https://github.com/slodge/BallControl/blob/master/Cirrious.Sphero.WorkBench/Cirrious.Sphero.WorkBench.Core.Test/ViewModels/HomeViewModelTest.cs

    For main questions, please try StackOverflow - just helps others when they search for the answers - plus there are tens (at least) of SQLite plugin users - so they may help answer too :)

    ReplyDelete