Handling PropertyChanged Event in MvvmCross View Model Unit Tests

March 24th 2014 MvvmCross Unit Testing

A great side effect of view models being implemented in portable class libraries when using MvvmCross, is the ability to unit test them on any platform, not necessarily the one you are targeting. So even when developing a mobile application, you can test it in full .NET framework and take advantage of all the tooling available there. In my opinion NCrunch test runner and Moq mocking framework are great examples of tools that are not available for mobile platforms but can make testing much more pleasant.

View model unit tests often depend on PropertyChanged events or have to test them. A typical simple view model unit test checking whether data gets loaded correctly on Start, could look like this:

[TestMethod]
public void LoadsPlayersOnStartAndNotifies()
{
    var expected = new[]
    {
        new Player(),
        new Player(),
        new Player()
    };

    var repositoryServiceMock = new Mock<IRepositoryService>();
    repositoryServiceMock.Setup(mock => mock.GetPlayers()).Returns(expected);
    var viewModel = new PlayersEditorViewModel(repositoryServiceMock.Object);

    var handle = new AutoResetEvent(false);
    viewModel.PropertyChanged += (sender, args) =>
    {
        if (args.PropertyName == "Players")
        {
            handle.Set();
        }
    };
    viewModel.Start();
    Assert.IsTrue(handle.WaitOne(TimeSpan.FromMilliseconds(50)));

    CollectionAssert.AreEqual(expected, viewModel.Players);
}

Contrary to what you would expect, the above test fails because the PropertyChanged event is never raised, even though the view model being tested, works correctly.

After taking a look at MvxNotifyPropertyChanged code, it was easy to explain the behavior. RaisePropertyChanged by default marshals PropertyChanged events to the UI thread (see ShouldAlwaysRaiseInpcOnUserInterfaceThread):

public virtual void RaisePropertyChanged(PropertyChangedEventArgs changedArgs)
{
    // check for interception before broadcasting change
    if (InterceptRaisePropertyChanged(changedArgs)
        == MvxInpcInterceptionResult.DoNotRaisePropertyChanged)
        return;

    var raiseAction = new Action(() =>
            {
                var handler = PropertyChanged;

                if (handler != null)
                    handler(this, changedArgs);
            });

    if (ShouldAlwaysRaiseInpcOnUserInterfaceThread())
    {
        // check for subscription before potentially causing a cross-threaded call
        if (PropertyChanged == null)
            return;

        InvokeOnMainThread(raiseAction);
    }
    else
    {
        raiseAction();
    }
}

In unit tests there's no Dispatcher set, causing InvokeOnMainThread not to raise the event at all:

protected void InvokeOnMainThread(Action action)
{
    if (Dispatcher != null)
        Dispatcher.RequestMainThreadAction(action);
}

Fortunately the problem is really easy to fix now, with all the information at hand. There's no need to marshal the events to UI thread in unit tests, therefore the corresponding view model setting can be disabled (notice the call to ShouldAlwaysRaiseInpcOnUserInterfaceThread(false)):

[TestMethod]
public void LoadsPlayersOnStartAndNotifies()
{
    var expected = new[]
    {
        new Player(),
        new Player(),
        new Player()
    };

    var repositoryServiceMock = new Mock<IRepositoryService>();
    repositoryServiceMock.Setup(mock => mock.GetPlayers()).Returns(expected);
    var viewModel = new PlayersEditorViewModel(repositoryServiceMock.Object);
    viewModel.ShouldAlwaysRaiseInpcOnUserInterfaceThread(false);

    var handle = new AutoResetEvent(false);
    viewModel.PropertyChanged += (sender, args) =>
    {
        if (args.PropertyName == "Players")
        {
            handle.Set();
        }
    };
    viewModel.Start();
    Assert.IsTrue(handle.WaitOne(TimeSpan.FromMilliseconds(50)));

    CollectionAssert.AreEqual(expected, viewModel.Players);
}

With this minor modification the unit test succeeds as expected.

There's still quite a lot of code involved in setting up the handling of PropertyChanged event in the unit test. Since a similar pattern is often required in tests, it can be wrapped in an extension method to make the tests more straightforward and less repetitive:

public static Task WaitPropertyChangedAsync(this MvxViewModel viewModel, 
    string propertyName)
{
    viewModel.ShouldAlwaysRaiseInpcOnUserInterfaceThread(false);

    var task = new Task(() => { });
    viewModel.PropertyChanged += (sender, args) =>
    {
        if (args.PropertyName == propertyName)
        {
            task.Start();
        }
    };
    return task;
}

Now the unit test can be much simpler and will only contain the code actually relevant to it:

[TestMethod]
public void LoadsPlayersOnStartAndNotifies()
{
    var expected = new[]
    {
        new Player(),
        new Player(),
        new Player()
    };

    var repositoryServiceMock = new Mock<IRepositoryService>();
    repositoryServiceMock.Setup(mock => mock.GetPlayers()).Returns(expected);
    var viewModel = new PlayersEditorViewModel(repositoryServiceMock.Object);
    var propertyChangedTask = viewModel.WaitPropertyChangedAsync("Players");

    viewModel.Start();
    Assert.IsTrue(propertyChangedTask.Wait(50));

    CollectionAssert.AreEqual(expected, viewModel.Players);
}
Copyright
Creative Commons License