Just some notes on Unit Testing MvvmCross.
#Setup Test Project
- Create an empty project
- NOTE: depending on how you setup your Xamarin project you might need multiple projects for iOS, Android and PCL
- Add the follow dependencies
- AutoFixture - Test Data Genertator
- Moq - Object Mocking framework
- MvvmCross
- NUnit
- Any other MvvmCross modules as needed
- Refence the
Now we should have all our tools we need for doing unit testing. So, lets create a test class
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Cirrious.CrossCore.Core;
using Cirrious.MvvmCross.Plugins.Messenger;
using Cirrious.MvvmCross.Test.Core;
using Cirrious.MvvmCross.Views;
using Moq;
using NUnit.Framework;
using Ploeh.AutoFixture;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Platform;
// Add any more project specific namespaces
namespace ProjectX.PCL.Tests
{
[TestFixture]
// You need to subclass MvxIoCSupportingTest
public class CustomerViewModelTests : MvxIoCSupportingTest {
// CustomerViewModel all dependentcies
Mock<ICustomerService> _customerService;
Mock<IStateFullyDependency> _stateFullyDependency;
// Test Data generator
readonly Fixture _fixture;
public CustomerViewModelTests() {
_fixture = new Fixture ();
}
// VM generator. This can help if you have a lot of dependentcies
CustomerViewModel GetModel => new CustomerViewModel (customerService.Object);
// Setup is called before each test is run
[SetUp]
public void init() {
base.ClearAll (); // This resets the IoC container.
// Setup
_customerService = new Mock<ICustomerService> ();
Ioc.RegisterSingleton<ICustomerService> (_customerService.Object);
}
// If you need to clean up anything after each test use the
// TearDown attribute
[TearDown]
public void Cleanup() {
// Code Dragons be here.
}
// add Tests
[Test]
public void TestNameHere() {}
}
}
I'm sure everyone has had it where they passed the wrong paramaters to a show viewModel I know I've had this happen a few tiems.
To make sure your VM is passing the correct keys to a ShowViewModel you will need
If we want to test our ViewModel is passing the expected keys to ShowViewModel we need to hook into the ShowViewModel method handler
First we need to subclass the MvxMainThreadDispatcher. In this case I'm only checking that all the keys I expect are present and are been passed to the ShowViewModel form the ViewModel
class NavMockDispatcher : MvxMainThreadDispatcher, IMvxViewDispatcher {
readonly List<string> _expectedKeys;
public NavMockDispatcher (List<string> expectedKeys) {
this._expectedKeys = expectedKeys;
}
public override bool ShowViewModel (MvxViewModelRequest request) {
var result = false;
foreach (var key in request.ParameterValues.Keys) {
result = _expectedKeys.Contains (key);
// Soon as we find a missing key exit the loop
if (!result) {
break;
}
}
// If we found all keys result should be true,
// If any keys where missing result should be false
Assert.IsTrue (result);
return result;
}
// We don't need these methods for the test. Here are the stubs in anycase
public virtual bool ChangePresentation (MvxPresentationHint hint)
{
throw new NotImplementedException ();
}
public virtual bool RequestMainThreadAction (Action action)
{
action ();
return true;
}
}
Now we have our MvxMainThreadDispatcher setup how do we test the CustomerViewModel ShowViewModel. In this example Lets assume we have a show banking view model method.
// .... CustomerViewModel
public bool ShowBankingView() {
ShowViewModel(new { customerId = _id, authToken = _bankToken } );
}
// .... CustomerViewModel
Then our test would look somthing like.
[Test]
public void ShowBankingView_Should_Pass_ShowViewModel_ExpectedKeys() {
setupDefaultServiceData ();
var mockDispatcher = new NavMockDispatcher (new List<string>{
"customerId", "authToken"
});
// Just for completness lets register mockDispatcher with both the IMvxViewDispatcher
// and MvxMainThreadDispatcher with the IoC
Ioc.RegisterSingleton<IMvxViewDispatcher> (mockDispatcher);
Ioc.RegisterSingleton<MvxMainThreadDispatcher> (mockDispatcher);
// You also need to register MvxStringToTypeParser.
// Not sure if this is needed when using a Dict<string,string>
// Since I'm using an anonymous classes it appears to be required
Ioc.RegisterSingleton<IMvxStringToTypeParser> (new MvxStringToTypeParser());
// Get a new model by calling our factory get property
var customerModel = GetModel;
customerModel.ShowBankingView ();
// Remeber the Assert is done inside the NavMock
}
If you have any Property Changed events that alter other backing stores apart from their own you may find your testing failing. Don't worry this is expected behaviour. To test property changed propagations you need to subclass the MvxMainThreadDispatcher. Like we did with the NavMock before we need to subclass the MvxMainThreadDispatcher.
In this case I'm going to make another subclass on the MvxMainThreadDispatcher. That's it you're test should now pass.
class NavMockDispatcher : MvxMainThreadDispatcher, IMvxViewDispatcher {
public override bool ShowViewModel (MvxViewModelRequest request) {
throw new NotImplementedException ();
}
public virtual bool ChangePresentation (MvxPresentationHint hint)
{
throw new NotImplementedException ();
}
public virtual bool RequestMainThreadAction (Action action)
{
action ();
return true;
}
}