Throughout this book, we've implemented patterns and best practices with the intention of separating the layers of our TripLog app, making it easier to maintain and test. Over the course of this chapter, we'll write unit tests for the business logic in our ViewModels.
In this chapter, we'll cover the following topics:
We'll start by adding a new NUnit project to our solution, to contain all of the unit tests we'll write throughout the rest of the chapter.
To test the business logic in our TripLog app, we'll start out by creating a new unit test project in our solution that will be responsible for testing our ViewModels. There are many options and libraries to create unit tests in .NET with Visual Studio. In this chapter, we will use the NUnit Library Project template in Visual Studio for Mac.
In order to create a unit test project, perform the following steps:
Tests
. Although this is not required, it helps keep any testing-related projects organized within the overall solution. To add a new solution folder in Visual Studio, simply right-click on the solution name, go to Add and click on Add Solution Folder, as shown in the following screenshot:
Figure 1: Adding a solution folder in Visual Studio
Tests
solution folder:Figure 2: Creating a new NUnit project in Visual Studio (step 1 of 2)
TripLog.Tests
, as shown in the following screenshot:Figure 3: Creating a new NUnit project in Visual Studio (step 2 of 2)
TripLog.Tests
project. Be sure to add the same version that's being used by the other projects in your solution. This reference is required due to the dependencies our ViewModels have on Xamarin.Forms, specifically for Command
properties.Test.cs
file. You can safely delete this file, since we'll create new ones that are specific to each of our ViewModels in the next section.Now that we have created a new test project, we can begin writing unit tests for our ViewModels.
When unit testing ViewModels, it is best to break the tests into individual test classes that represent each ViewModel, resulting in a one-to-one relationship between ViewModel classes and the unit test classes that test their logic.
In order to test our ViewModels, we will need to add a reference to them within the unit tests project. To do this, right-click the References
folder within the TripLogs.Tests
project, then click on Edit References, and then select the TripLog core library project, as shown in the following screenshot:
Figure 4: Adding a TripLog project reference to the Unit Test project in Visual Studio
We will start by creating a set of unit tests for the DetailViewModel
:
TripLog.Tests
project, named ViewModels
. This helps keep the file structure of the tests the same as the library being tested.DetailViewModelTests
within the new ViewModels
folder in the TripLog.Tests
project.DetailViewModelTests
class with a TestFixture
attribute:using NUnit.Framework;
[TestFixture]
public class DetailViewModelTests
{
}
DetailViewModelTests
class by adding a new method named Setup
with the [SetUp]
NUnit attribute, as follows:[TestFixture]
public class DetailViewModelTests
{
[SetUp]
public void Setup()
{
}
}
This Setup
method will be responsible for creating new instances of our ViewModel for each of the tests within the class by ensuring that each test is run with a clean, known state of the ViewModel under test.
In order to create a new instance of a ViewModel, we need to provide it with the instances of the services required by its constructor. During runtime, these are automatically provided via constructor injection, but in the case of the unit tests, we'll need to provide them manually. We have a couple of options for passing in these services.
We can create new mock versions of our services and pass them into the ViewModel's constructor. This requires providing a mock implementation for each method in the service's interface, which can be time-consuming and causes additional code maintenance.
We can also use a mocking library to create mocks of the services and pass these mocks into the ViewModel's constructor. The mocking library provides a much cleaner approach, that's also less fragile. Additionally, most mocking libraries provide a way to specify how methods or properties should return data in a much cleaner way without actually having to implement them ourselves. In this chapter, we will use Moq (available on NuGet)—a very popular mocking library for .NET applications—to handle mocking for our unit tests.
In order to initialize the ViewModel with mocked services, perform the following steps:
TripLog.Tests
project.Setup
method, create a new instance of DetailViewModel
and use the Moq library to create a mock instance of INavService
to pass in when instantiating DetailViewModel
:using NUnit.Framework;
using Moq;
using TripLog.Services;
using TripLog.ViewModels;
[TestFixture]
public class DetailViewModelTests
{
DetailViewModel _vm;
[SetUp]
public void Setup()
{
var navMock = new Mock<INavService>().Object;
_vm = new DetailViewModel(navMock);
}
}
Now that we have a setup function defined, we can create an actual test method. This ViewModel does not do much beyond initialization. Therefore, we'll just test the Init
method, to ensure that the ViewModel is properly initialized when its Init
method is called. The success criteria for this particular test will be that once Init
is called, the Entry
property of the ViewModel will be set to the value provided in the Init
method's parameter.
In order to create a test for the ViewModel's Init
method, perform the following steps:
DetailViewModelTests
, named Init_ParameterProvided_EntryIsSet
, and decorate it with an NUnit Test
attribute. Each test method that we create will follow the Arrange-Act-Assert pattern:[TestFixture]
public class DetailViewModelTests
{
// ...
[Test]
public void Init_ParameterProvided_EntryIsSet()
{
// Arrange
// Act
// Assert
}
}
The Arrange-Act-Assert pattern is a popular approach to laying out unit test methods.
The Arrange
portion is where you set up any preconditions needed for the test.
The Act
portion is where you call the code that is under test.
The Assert
portion is where you confirm the code that is under test behaves as expected.
TripLogEntry
object, to pass to the Init
method in order to test its functionality. Also, set the ViewModel's Entry
property to null, so that we can easily confirm that the property has a proper value after calling Init
later, in the assert portion of the test:using NUnit.Framework;
using Moq;
using TripLog.Services;
using TripLog.ViewModels;
using TripLog.Models;
[TestFixture]
public class DetailViewModelTests
{
// ...
[Test]
public void Init_ParameterProvided_EntryIsSet()
{
// Arrange
var mockEntry = new Mock<TripLogEntry>().Object;
_vm.Entry = null;
// Act
// Assert
}
}
TripLogEntry
object into the ViewModel's Init
method in the act portion of the test method:[Test]
public void Init_ParameterProvided_EntryIsSet()
{
// Arrange
var mockEntry = new Mock<TripLogEntry>().Object;
_vm.Entry = null;
// Act
_vm.Init(mockEntry);
// Assert
}
Entry
property is no longer null
using the NUnit Assert.IsNotNull
method:[Test]
public void Init_ParameterProvided_EntryIsSet()
{
// Arrange
var mockEntry = new Mock<TripLogEntry>().Object;
_vm.Entry = null;
// Act
await _vm.Init(mockEntry);
// Assert
Assert.IsNotNull(_vm.Entry, "Entry is null after being initialized with a valid TripLogEntry object");
}
There are several other Assert
methods, such as AreEqual
, IsTrue
, and IsFalse
, which can be used for various types of assertions.
Notice the second parameter in the Assert.IsNotNull
method usage in step 4, which is an optional parameter. This allows you to provide a message to be displayed if the test fails, to help troubleshoot the code under the test.
We should also include a test to ensure that the ViewModel throws an exception if the empty Init
method is called, because the DetailViewModel
requires the use of the Init
method in the base class that takes a parameter. We can do this using the Assert.Throws
NUnit method and providing a delegate that calls the Init
method:
[TestFixture]
public class DetailViewModelTests
{
// ...
[Test]
public void Init_ParameterNotProvided_ThrowsEntryNotProvidedException()
{
// Assert
Assert.Throws(typeof(EntryNotProvidedException), () => _vm.Init());
}
}
Initially, this test will fail because, until this point, we haven't included the code to throw an EntryNotProvidedException
in DetailViewModel
. In fact, the tests currently won't even build, because we've not defined the EntryNotProvidedException
type.
In order to get the tests to build, create a new class in the core library that inherits from Exception
and name it EntryNotProvidedException
:
using System;
public class EntryNotProvidedException : Exception
{
public EntryNotProvidedException()
: base("An Entry object was not provided. If using DetailViewModel, be sure to use the Init overload that takes an Entry parameter.")
{
}
}
For ViewModels that have dependencies on a specific functionality of a service, you'll need to provide some additional setup when you mock the objects for its constructor. For example, the NewEntryViewModel
depends on the GetGeoCoordinatesAsync
method of ILocationService
in order to get the user's current location in the Init
method. By simply providing a new Mock
object for ILocationService
to the ViewModel, this method will return null
, and an exception will be thrown when setting the Latitude
and Longitude
properties. In order to overcome this, we just need to use the Setup
method when creating the Mock
, to define how the calls to the GetGeoCoordinatesAsync
method should be returned to the callers of the mock ILocationService
instance. This allows us to test a specific ViewModel functionality without needing to deal with the implementation of a specific dependency – in fact, it ensures that the dependency always returns the same results, so the functionality being tested can be tested consistently.
To see this in action, we'll create a unit test to test the NewEntryViewModel
Init
method to assert that whenever it is called it gets the current location and sets the Latitude
and Longitude
properties, as shown in the following steps:
TripLog.Tests
project named NewEntryViewModelTests
. Add the TextFixture
attribute to the class, just as we did with the DetailViewModelTests
class:using NUnit.Framework;
[TestFixture]
public class NewEntryViewModelTests
{
}
Setup
with the [SetUp]
attribute, where we will define the NewEntryViewModel
instance that will be used by tests in the class. NewEntryViewModel
requires three parameters. We will use Moq again to provide mock implementations for them, but we will need to customize the implementation for ILocationService
to specify exactly what the GetGeoCoordinatesAsync
method should return:using NUnit.Framework;
using Moq;
using TripLog.Models;
using TripLog.Services;
using TripLog.ViewModels;
[TestFixture]
public class NewEntryViewModelTests
{
NewEntryViewModel _vm;
Mock<INavService> _navMock;
Mock<ITripLogDataService> _dataMock;
Mock<ILocationService> _locMock;
[SetUp]
public void Setup()
{
_navMock = new Mock<INavService>();
_dataMock = new Mock<ITripLogDataService>();
_locMock = new Mock<ILocationService>();
_locMock.Setup(x => x.GetGeoCoordinatesAsync())
.ReturnsAsync(new GeoCoords
{
Latitude = 123,
Longitude = 321
});
_vm = new NewEntryViewModel(_navMock.Object, _locMock.Object, _dataMock.Object);
}
}
Now that we know our mock ILocationService
implementation will return 123
for Latitude
and 321
for Longitude
, we can properly test the ViewModel's Init
method and ensure that the Latitude
and Longitude
properties are properly set using its provided ILocationService
(this would be an actual platform-specific implementation when running the mobile app).
Following the Arrange-Act-Assert pattern, set the values of the Latitude
and Longitude
properties to 0
before calling the Init
method. In the assert portion of the test, we confirm that after calling Init
, the Latitude
and Longitude
properties of ViewModel are the values that we expect to come from the provided mock ILocationService
instance—in our case, 123
and 321
:
[TestFixture]
public class NewEntryViewModelTests
{
// ...
[Test]
public void Init_EntryIsSetWithGeoCoordinates()
{
// Arrange
_vm.Latitude = 0.0;
_vm.Longitude = 0.0;
// Act
_vm.Init();
// Assert
Assert.AreEqual(123, _vm.Latitude);
Assert.AreEqual(321, _vm.Longitude);
}
}
It is important to recognize that we're not testing the actual result or functionality of the ILocationService
method—we're testing the behavior of the Init
method, which depends on the ILocationService
method. The best way to do this is with mock objects—especially for platform-specific services or services that provide dynamic or inconsistent data.
There are a few more unit tests we can write for the NewEntryViewModel
, to increase its test coverage. We should write a test to assert that the Save button is not enabled if the Title
field has not been provided. This can be done by testing the SaveCommand
's CanExecute
function, as follows:
[TestFixture]
public class NewEntryViewModelTests
{
// ...
[Test]
public void SaveCommand_TitleIsEmpty_CanExecuteReturnsFalse()
{
// Arrange
_vm.Title = "";
// Act
var canSave = _vm.SaveCommand.CanExecute(null);
// Assert
Assert.IsFalse(canSave);
}
}
Next, we'll write some tests that assert that when the SaveCommand
is executed, it actually sends the TripLogEntry
object to the data service and then navigates the user back to the main page. In order to test that specific methods on a service are called, we can mark them as Verifiable
when setting up the service mocks in the text fixture Setup
method, and then call the Verify
method in the unit tests to verify they're called, as shown in the following steps:
Setup
method in NewEntryViewModelTests
to set up the INavService
and ITripLogDataService
mocks so the methods used by the SaveCommand
are verifiable:[TestFixture]
public class NewEntryViewModelTests
{
NewEntryViewModel _vm;
Mock<INavService> _navMock;
Mock<ITripLogDataService> _dataMock;
Mock<ILocationService> _locMock;
[SetUp]
public void Setup()
{
_navMock = new Mock<INavService>();
_dataMock = new Mock<ITripLogDataService>();
_locMock = new Mock<ILocationService>();
_navMock.Setup(x => x.GoBack())
.Verifiable();
_dataMock.Setup(x => x.AddEntryAsync(It.Is<TripLogEntry>(entry => entry.Title == "Mock Entry")))
.Verifiable();
_locMock.Setup(x => x.GetGeoCoordinatesAsync())
.ReturnsAsync(new GeoCoords
{
Latitude = 123,
Longitude = 321
});
_vm = new NewEntryViewModel(_navMock.Object, _locMock.Object, _dataMock.Object);
}
// ...
}
Notice how the setup for the AddEntryAsync
method is for a TripLogEntry
instance that specifically has a Title
equal to "Mock Entry." This is how we can later verify that not only are we calling the AddEntryAsync
method, but we are passing the correct data to it.
SaveCommand_AddsEntryToTripLogBackend
that executes the SaveCommand
, and verifies that the TripLogEntry
object created in the ViewModel is actually passed to the AddEntryAsync
method:[TestFixture]
public class NewEntryViewModelTests
{
// ...
[Test]
public void SaveCommand_AddsEntryToTripLogBackend()
{
// Arrange
_vm.Title = "Mock Entry";
// Act
_vm.SaveCommand.Execute(null);
// Assert
_dataMock.Verify(x => x.AddEntryAsync(It.Is<TripLogEntry>(entry => entry.Title == "Mock Entry")), Times.Once);
}
}
SaveCommand_NavigatesBack
that executes the SaveCommand
and verifies that the app navigates back:[TestFixture]
public class NewEntryViewModelTests
{
// ...
[Test]
public void SaveCommand_NavigatesBack()
{
// Arrange
_vm.Title = "Mock Entry";
// Act
_vm.SaveCommand.Execute(null);
// Assert
_navMock.Verify(x => x.GoBack(), Times.Once);
}
}
We have now written several tests that assert the various behaviors of the NewEntryViewModel
. As you can see, the use of dependency injection in the app architecture makes it extremely easy to test our ViewModels with maximum flexibility and minimum code. Next, we will run these unit tests in Visual Studio to see if they pass or fail.
Once you have some unit tests created, you can start running them directly from Visual Studio. To run tests in Visual Studio for Mac, simply click on Run Unit Tests from the Run menu; in Visual Studio for Windows, click Run > All Tests from the Test menu. Typically, this should be done as tests are created throughout your development lifecycle as well as before you commit your code to source control, especially if there is a continuous integration process that will automatically build your code and run the tests.
After the tests have completed running, the results will appear in the Test Results pane:
Figure 5: Unit test results in Visual Studio
Notice that one of our unit tests is failing. In order to get this test to pass, we need to go back and update DetailViewModel
by overriding the empty Init
method of BaseViewModel
, and have it throw a new EntryNotProvidedException
instance, as follows; this type of iterative testing development process is a common best practice, which helps you develop better code with more testing coverage:
public class DetailViewModel : BaseViewModel<TripLogEntry>
{
// ...
public override void Init()
{
throw new EntryNotProvidedException();
}
public override void Init(TripLogEntry logEntry)
{
Entry = logEntry;
}
}
Now, when you rerun the unit tests, they should all pass:
Figure 6: Unit test results in Visual Studio
In this chapter, we looked into how to take advantage of the loosely coupled architecture that we developed in the earlier chapters of this book to write unit tests. We used a mocking framework to mock out the services that our ViewModels are dependent on, to be able to effectively test the logic within them in a predictable manner. In the next chapter, we'll add the ability to monitor app usage and crashes in our TripLog mobile app.