Chapter 8. Testing

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:

  • Unit testing with NUnit
  • Automated UI testing with the Xamarin.UITest framework

Unit testing

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:

  1. Create a new solution folder named 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:
    Unit testing

    Figure 1 Creating a new solution folder for testing related projects

  2. Next, create a new NUnit project in the new Tests solution folder and name it TripLog.Tests.
    Unit testing
    Unit testing

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.

Testing 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 reference them within the unit tests project.

Add a reference to the TripLog core library project to the TripLog.Tests project.

Testing ViewModels

We will start by creating a set of unit tests for DetailViewModel:

  1. Create a new empty class named DetailViewModelTests within the new TripLog.Tests project.
  2. Next, update the DetailViewModelTests class with a TestFixture attribute:
    [TestFixture]
    public class DetailViewModelTests
    { }
  3. Next, create a test 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:

  1. Add a reference to the Moq NuGet package to the TripLog.Tests project.
  2. Next, within the 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:

  1. Create a new method in 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
        }
    }
  2. Next, update the Arrange portion of the test method by creating a new mocked instance of a 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
    }
  3. Next, pass the mocked 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
    }
  4. Finally, check that the ViewModel's Entry property is no longer 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.");
    }

    Tip

    There are several other Assert methods such as AreEqual, IsTrue, and IsFalse that can be used for various types of assertions.

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:

  1. Create a new class in the TripLog.Tests project named NewEntryViewModelTests. Add the TextFixture attribute to the class, just as we did with the DetailViewModelTests class:
    [TestFixture]
    public class NewEntryViewModelTests
    { }
  2. Next, create a 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.

Running unit tests in Xamarin Studio

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:

Running unit tests in Xamarin Studio

If the test fails, you will see the failure along with the Stack Trace in the Test Results pane:

Running unit tests in Xamarin Studio

Notice the message that we provided in the Assert.IsNotNull method is shown in the failure result.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.17.18