Unit Testing Asynchronous UI Code in WinRT

December 9th 2013 Windows Store Unit Testing Async

Writing unit tests for code that needs to be run on the UI thread can be quite a challenge in WinRT. On top of that one can quickly stumble upon classes required to run on the UI thread, even when not expecting them to. In a project I worked on a while ago WriteableBitmap was one such class.

It all started when I tried to write my first unit test with that class (I removed most of the actual code for clarity):

[TestMethod]
public void WriteableBitmapTest()
{
    var bitmap = new WriteableBitmap(100, 100);
    bitmap.DrawEllipseCentered(50, 50, 30, 30, Colors.Blue);
}

Fortunately the error message returned by Visual Studio 2013 test runner is quite clear about the problem:

The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD)). If you are using UI objects in test consider using [UITestMethod] attribute instead of [TestMethod] to execute test in UI thread.

Hence my next attempt:

[UITestMethod]
public void UiThreadTest()
{
    var bitmap = new WriteableBitmap(100, 100);
    bitmap.DrawEllipseCentered(50, 50, 30, 30, Colors.Blue);
}

This works; at least until you try to add an asynchronous method call to the test:

[UITestMethod]
public async Task AsyncTestOnUiThread()
{
    var bitmap = new WriteableBitmap(100, 100);
    bitmap.DrawEllipseCentered(50, 50, 30, 30, Colors.Blue);

    await SaveToFileAsync(bitmap);
}

The test runner responds to such an attempt with a different error message:

async TestMethod with UITestMethodAttribute are not supported. Either remove async or use TestMethodAttribute.

This leaves us with little choice:

  • TestMethodAttribute supports asynchronous test methods but doesn't run them on UI thread.
  • UITestMethodAttribute runs test methods on UI thread but doesn't support asynchronous methods.

That's where I got stuck for a while and decided not to write a couple of tests that I planned to. A Stack Overflow answer by chue x inspired me to give it another try. This time I stuck with TestMethodAttribute and took a different approach to both getting the code executed on the UI thread and to calling asynchronous methods:

[TestMethod]
public async Task TestAsyncCodeOnUiThread()
{
    var taskSource = new TaskCompletionSource<object>();
    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal, async () =>
    {
        try
        {
            var bitmap = new WriteableBitmap(100, 100);
            bitmap.DrawEllipseCentered(50, 50, 30, 30, Colors.Blue);

            await SaveToFileAsync(bitmap);
            taskSource.SetResult(null);
        }
        catch (Exception e)
        {
            taskSource.SetException(e);
        }
    });
    await taskSource.Task;
}

Using TestMethodAttribute allowed me to make the test method asynchronous. CoreDispatcher.RunAsync now takes care of running my code on the UI thread. Finally the test works as I wanted it to.

There is another very important detail in my test I would like to bring attention to, i.e. the use of TaskCompletionSource<T>. This is required to properly synchronize the code in lambda passed to RunAsync with the remaining code in the test method. Although it might not appear so, the lambda returns void (as declared by DispatchedHandler) and therefore is not guaranteed to complete before the test method body execution continues, since it isn't awaited. By awaiting the TaskCompletionSource<T>.Task as shown above instead, the test method execution will only continue after SetResult is called at the end of the lambda. The try-catch statement inside the lambda makes sure that any exceptions thrown in its body will be properly caught by the test runner instead of making it crash.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License