Throughout the book, we have implemented patterns and best practices with the intention of separating the layers of our TripLog app, making it easier to maintain and test. In this chapter, we will write tests for our business logic by unit testing ViewModels, and we will write tests for our UI with automated UI testing.
Here is a quick look at what we will cover in this chapter:
We will start by testing the business logic in the TripLog app. We will do this 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. By default, Xamarin Studio leverages the popular NUnit framework for performing unit tests.
In order to create a unit test project, perform the following steps:
Tests
. While this is not required, it helps keep any testing-related projects organized within the overall solution. To add a new solution folder in Xamarin Studio, simply right click on the solution, go to Add and click on Add Solution Folder, as seen in Figure 1:Tests
solution folder and name it TripLog.Tests
.By default, the new NUnit project will contain a file named Test.cs. You can safely delete this file, as we will create new ones that are specific to each of our ViewModels in the next section.
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 reference them within the unit tests project.
Add a reference to the TripLog core library project to the TripLog.Tests
project.
We will start by creating a set of unit tests for DetailViewModel
:
DetailViewModelTests
within the new TripLog.Tests
project.DetailViewModelTests
class with a TestFixture
attribute:[TestFixture]
public class DetailViewModelTests
{ }
Setup
function in the DetailViewModelTests
class:[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 dependency injection, but in the case of the unit tests, we need to provide them manually. We have a couple of options here; 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 it causes additional code maintenance. We can also use a mocking library to create mocks of the services, and we can pass these mocks into the ViewModel's constructor. The mocking library provides a much cleaner approach that is also less fragile. Additionally, most mocking libraries will provide a way to specify how methods or properties should return data in a much cleaner way without actually having to implement them ourselves. I like to use Moq
(available on NuGet)—a very popular mocking library for .NET applications—to handle mocking for my unit tests.
In order to initialize the ViewModel with mocked services, perform the following steps:
Moq
NuGet package to the TripLog.Tests
project.Setup
method, create a new instance of DetailViewModel
. Use the Moq
library to create a mock instance of INavService
to pass in when instantiating DetailViewModel
:[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 will just test the Init
method to make sure 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 async Task Init_ParameterProvided_EntryIsSet() { // Arrange // Act // Assert } }
TripLogEntry
object to pass to the Init
method in order to test its functionality. Also, set the ViewModel's Entry
property to null
so we can easily confirm that the property has a proper value after calling Init
later in the Assert portion of the test.[Test] public async Task 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 async Task Init_ParameterProvided_EntryIsSet()
{
// Arrange
var mockEntry = new Mock<TripLogEntry> ().Object;
_vm.Entry = null;
// Act
await _vm.Init (mockEntry);
// Assert
}
null
using the NUnit Assert.IsNotNull
method:[Test]
public async Task 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.");
}
Notice the second parameter in the Assert.IsNotNull
method, 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 include a test to ensure 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:
[Test] public void Init_ParameterNotProvided_ThrowsEntryNotProvidedException() { // Assert Assert.Throws (typeof (EntryNotProvidedException), async () => { await _vm.Init (); }); }
Initially, this test will fail because until this point, we have not included the code to throw an EntryNotProvidedException
in DetailViewModel
. In fact, the tests currently will not even build because we have 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
:
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.") { } }
Now, if we run the tests, the first one will pass; and the second test, the one that asserts that calls to an empty Init
method throws an exception, will fail. In order to get this test to pass, we just need to override the empty Init
method in DetailViewModel
and have it throw EntryNotProvidedException
:
public class DetailViewModel : BaseViewModel<TripLogEntry> { // ... public override async Task Init () { throw new EntryNotProvidedException (); } public override async Task Init (TripLogEntry logEntry) { Entry = logEntry; } }
For ViewModels that have dependencies on specific functionality of a service, you will need to provide an 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 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 Mock
to define how the calls to the GetGeoCoordinatesAsync
method should be returned to the callers of the mock ILocationService
instance:
TripLog.Tests
project named NewEntryViewModelTests
. Add the TextFixture
attribute to the class, just as we did with the DetailViewModelTests
class:[TestFixture]
public class NewEntryViewModelTests
{ }
Setup
method with the [Setup]
attribute where we will define the NewEntryViewModel
instance that will be used by tests in the class. NewEntryViewModel
requires four parameters. We will use Moq
again to provide mock implementations for them, but we need to customize the implementation for ILocationService
to specify exactly what the GetGeoCoordinatesAsync
method should return. By doing this, we can properly assert all the values in the ViewModel that come from the GetGetoCoordinatesAsync
method:[TestFixture] public class NewEntryViewModelTests { NewEntryViewModel _vm; [SetUp] public void Setup() { var locMock = new Mock<ILocationService> (); locMock .Setup (x => x.GetGeoCoordinatesAsync ()) .ReturnsAsync ( new GeoCoords { Latitude = 123, Longitude = 321 } ); _vm = new NewEntryViewModel ( new Mock<INavService> ().Object, locMock.Object, new Mock<ITripLogDataService> ().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 by 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, 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—123
and 321
:
[TestFixture] public class NewEntryViewModelTests { // ... [Test] public async Task Init_EntryIsSetWithGeoCoordinates() { // Arrange _vm.Latitude = 0.0; _vm.Longitude = 0.0; // Act await _vm.Init (); // Assert Assert.AreEqual (123, _vm.Latitude); Assert.AreEqual (321, _vm.Longitude); } }
It is important to recognize that we are not testing the actual result of the ILocationService
method—we are testing that the Init
method that calls the ILocationService
method will properly run and process the results of that method. The best way to do this is with mock objects—especially for platform-specific services or services that provide dynamic or inconsistent data.
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.
Once you have some unit tests created, you can start running them directly from Xamarin Studio. 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 build pipeline that will automatically run the tests.
To run the unit tests that we just created, navigate to Run | Run Unit Tests with the TripLog.Tests
project selected in the Solution pane; otherwise, right-click on the TripLog.Tests
project in the Solution pane and click on Run. After the tests have completed running, the results will appear in the Test Results pane:
If the test fails, you will see the failure along with the Stack Trace in the Test Results pane:
Notice the message that we provided in the Assert.IsNotNull
method is shown in the failure result.
3.149.250.11