8

Testing

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:

  • Adding a unit test project to our solution
  • Writing unit tests for some of the ViewModels in our app
  • Running unit tests and fixing a failing test

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.

Unit testing

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:

  1. Create a new solution folder in the TripLog solution named 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

    Adding a new solution folder in Visual Studio for Windows is the same process: right-click on the solution name, go to Add, and click on New Solution Folder.

  2. Next, create a new NUnit Library Project within the new Tests solution folder:

    Figure 2: Creating a new NUnit project in Visual Studio (step 1 of 2)

  3. Name the NUnit Library Project TripLog.Tests, as shown in the following screenshot:

    Figure 3: Creating a new NUnit project in Visual Studio (step 2 of 2)

  4. Add a reference to the Xamarin.Forms NuGet package to the 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.
  1. By default, the new NUnit project will contain a 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.

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 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:

  1. First, create a new folder in the TripLog.Tests project, named ViewModels. This helps keep the file structure of the tests the same as the library being tested.
  2. Next, create a new empty class named DetailViewModelTests within the new ViewModels folder in the TripLog.Tests project.
  3. Next, update the DetailViewModelTests class with a TestFixture attribute:
    using NUnit.Framework;
    [TestFixture]
    public class DetailViewModelTests
    {
    }
    
  1. Then, create a test setup method in the 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:

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

  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 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.

  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 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
        }
    }
    
  3. Next, pass the mocked 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
    }
    
  1. Finally, verify that the ViewModel's 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:

  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:
    using NUnit.Framework;
    [TestFixture]
    public class NewEntryViewModelTests
    {
    }
    
  2. Next, create a method named 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:

  1. Update the 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.

  2. Add a new test method named 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);
        }
    }
    
  1. Finally, add another test method named 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.

Running unit tests in Visual Studio

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

Summary

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.

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

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