Thursday, 22 November 2012

Unit testing a C++/CX app

There are many ways that a Windows Store app should be tested, including:

  • Unit testing
  • Integration testing
  • UX testing
  • Security testing
  • Localization testing
  • Accessibility testing
  • Performance testing
  • Device testing

Hilo was designed for testability and so all of these approaches were used. However, unit testing C++/CX code presented it’s own problems – specifically how to test asynchronous code, and code that must run on the UI thread.

In this blog post I’ll extend the sample photo viewing app I’ve previously developed to include unit tests. In Visual Studio, you can create a unit test project for apps written in C++/CX by using the Native Test Project template.

Implementation

The PhotoViewerTests project contains helper classes, a mock repository class, links to the classes under test, and the unit tests themselves. The unit tests can be ran by selecting Test > Run > All Tests in Visual Studio.MK

A simple unit test is shown below, from the PhotoViewModelTests class. It tests that the PhotoViewModel constructor initializes the GreyscaleCommand property. The unit test demonstrates testing code that must run on the UI thread.

TEST_METHOD(PhotoViewModelShouldSetupGreyscaleCommandWhenConstructed)
{
    auto vm = make_shared<PhotoViewModel^>(nullptr);
    TestHelper::RunUISynced([this, vm]()
    {
        (*vm) = ref new PhotoViewModel(m_repository);
    });
    Assert::IsNotNull((*vm)->GreyscaleCommand);
}

This unit test creates a shared pointer instance of the PhotoViewModel class, and then initializes the shared pointer instance to a new instance of the PhotoViewModel class, as a lambda embedded in the call to the RunUISynced method. RunUISynced is a helper method provided in the project that enables code that must be executed on the UI thread to be tested through unit tests. Notice that the PhotoViewModel constructor accepts a shared pointer of type Repository. The PhotoViewerTests project provides a class named StubRepository which implements the Repository type. The m_repository member variable is initialized to a shared pointer instance of the StubRepository class in the Initialize method of the PhotoViewModelTests class. The unit test then asserts that the GreyscaleCommand property of the PhotoViewModel class has been initialized to a value, and hence is not null.

A more complicated unit test is shown below, from the ThumbnailConverterTests class. It tests that the Convert method of the ThumbnailConverter class can convert a thumbnail to a BitmapImage. The unit test demonstrates testing code that must run on the UI thread, and code that’s asynchronous.

TEST_METHOD(ThumbnailConverterCanConvertThumbnailToBitmapImage)
{
    TestImageGenerator imageGenerator;
    Object^ bitmap = nullptr;
 
    TestHelper::RunUISynced([this, &bitmap, &imageGenerator]()
    {
        task_status status;
        auto fi = make_shared<FileInformation^>(nullptr);
        auto createTestImageTask = imageGenerator.CreateTestImageFileFromLocalFolder("UnitTestLogo.png", "TestFile.png")
            .then([fi](FileInformation^ file)
        {
            (*fi) = file;
            return file->GetThumbnailAsync(ThumbnailMode::PicturesView);
        });
 
        auto stream = TestHelper::RunSynced(createTestImageTask, status);
        auto thumbnailConverter = ref new ThumbnailConverter();
        TypeName bitmapImageTypeName = { "BitmapImage", TypeKind::Metadata };
        bitmap = thumbnailConverter->Convert(stream, bitmapImageTypeName, nullptr, "en-GB");
    });
 
    Assert::IsNotNull(bitmap);
 
    TestHelper::RunUISynced([&imageGenerator]()
    {
        task_status status;
        TestHelper::RunSynced(imageGenerator.DeleteFilesAsync(), status);
    });
}

As a lambda embedded in the call to the RunUISynced method, this unit test creates a shared pointer instance of a FileInformation object, and then creates a task that will call the CreateTestImageFileFromLocalFolder method to create a new file named TestFile.png from the UnitTestLogo.png file that’s one of the projects resources. The CreateTestImageFileFromLocalFolder method is a helper method in the TestImageGenerator class. Once the file is created, GetThumbnailAsync is called in order to retrieve the thumbnail for the file. The task is then executed through the call to the RunSynced method. RunSynced is a helper method that enables asynchronous operations to be tested through unit tests. An instance of the ThumbnailConverter class is then created, and then the Convert method is called on the instance in order to convert the retrieved thumbnail to a BitmapImage.The unit test then asserts that the bitmap local variable is not null. Finally, a lambda embedded in the call to the RunUISynced method calls the DeleteFilesAsync method to delete the previously created TestFile.png file. The DeleteFilesAsync method is a helper method in the TestImageGenerator class.

Please note that while the PhotoViewerTests project contains other unit tests, they are illustrative rather than exhaustive. For more information about writing unit tests in C++, see Writing Unit tests for C/C+ with the Microsoft Unit Testing Framework for C++.

Summary


In this blog post I’ve extended the sample photo viewing app to include a unit test project. The unit test project tests business logic in the app, and contains helper methods that enable you to unit test both asynchronous code, and code that must execute on the UI thread.

The sample app, including the unit test project, can be downloaded here. To download the unit and integration test project for Hilo, see Hilo’s codeplex site.

4 comments:

  1. Do you know if the Hilo C++ example will be updated for Windows 10 and VS2015? Love how complete it was, especially since it had great examples of how to Unit Test!

    ReplyDelete
    Replies
    1. I've no idea if it'll be updated. Everyone who worked on it no longer works for Microsoft.

      Delete
  2. Do you have any other references to how to best unit test Windows 10 Universal apps written in C++/Cx with VS2015? Thanks!

    ReplyDelete