xUnit.net and AsyncEnumerator to Async and Await

May 06, 2012 | xUnit.net | Async | Unit Testing

Prior to .NET 4, one had to implement the APM model in order to expose asynchronous methods. After a couple of years, the AsyncEnumerator class came out to simplify the APM by leveraging the use of C# iterators for asynchrony. In the meantime, Microsoft developed a new model for asynchronous (and parallel) programming. Since the new model was targeting the .NET 4, code written in previous versions have to keep using the AsyncEnumerator class.

have been using the AsyncEnumerator class since 2008 and it works great. Today we have the new Async and Await keywords in C# 5.0 (together with the many improvements in the CLR) that will ship with .NET 4.5 and the recently added support for async unit tests on version 1.9 of the xUnit.net. So, I decided to move some .NET 2.0 code using AsyncEnumerator to .NET 4.5 using Async and Await.

Below is an interface of the sample type (described in this article) exposing both synchronous and asynchronous versions of a time-consuming method:

public interface IMyType
{
    // Synchronous version of time-consuming method.
    DateTime DoSomething();

    // Asynchronous version of time-consuming method (Begin part).
    IAsyncResult BeginDoSomething(AsyncCallback callback, object state = null);

    // Asynchronous version of time-consuming method (End part).
    DateTime EndDoSomething(IAsyncResult asyncResult);
}

A unit test with the AsyncEnumerator can be similar to the one shown below:

public IEnumerator<int> D(AsyncEnumerator ae)
{
    var sut = new MyType(5);

    sut.BeginDoSomething(ae.End(), state: null);
    yield return 1;
    var result = sut.EndDoSomething(ae.DequeueAsyncResult());
}

However, since xUnit.net does not support methods of type IEnumerator<int>, we need to tell xUnit.net how to execute them:

[Fact]
public void D()
{
    // Drive the D method's iterator asynchronously.
    var ae = new AsyncEnumerator<DateTime>();
    ae.EndExecute(
        ae.BeginExecute(
            this.D(ae), _ => { }));
}

Moving to .NET 4.5 and xUnit.net 1.9 we can create an Extension Method that returns a Task in order to use both the Async and Await keywords in production code and the async unit tests feature of xUnit.net.

// Task-based asynchronous version of time-consuming method.
public static Task<DateTime> DoSomethingAsync(this IMyType t)
{
    return Task<DateTime>.Factory.FromAsync(
        t.BeginDoSomething,
        t.EndDoSomething,
        state: null
        );
}

Now the previous unit test with the AsyncEnumerator can be rewritten as follow:

[Fact]
public async Task D()
{
    var sut = new MyType(5);
    var result = await sut.DoSomethingAsync();
}

This looks very nice and clean. As it seems though, if the class contains many async unit tests they will not run in parallel. 

As an example, the following 3 tests will take 3 x 5 = 15 seconds to complete:

[Fact]
public async Task A()
{
    var sut = new MyType(5);
    var result = await sut.DoSomethingAsync();
}

[Fact]
public async Task B()
{
    var sut = new MyType(5);
    var result = await sut.DoSomethingAsync();
}

[Fact]
public async Task C()
{
    var sut = new MyType(5);
    var result = await sut.DoSomethingAsync();
}

The output from xUnit.net:

Output from AsyncUnitTesting.xUnit.net.MyTypeTests.C:
  Started  5/5/2012 10:24:45 AM
  Finished 5/5/2012 10:24:50 AM

Output from AsyncUnitTesting.xUnit.net.MyTypeTests.B:
  Started  5/5/2012 10:24:50 AM
  Finished 5/5/2012 10:24:55 AM

Output from AsyncUnitTesting.xUnit.net.MyTypeTests.A:
  Started  5/5/2012 10:24:50 AM
  Finished 5/5/2012 10:25:00 AM

3 passed, 0 failed, 0 skipped, took 15.26 seconds (xUnit.net 1.9.0 build 1566).

That was the part of moving the unit tests from an older version of xUnit.net (and the AsyncEnumerator) to version 1.9 of xUnit.net (with Async and Await). Apparently, things become more challenging when moving to production code where one needs to deal with stuff such is the SyncrhonizationContext.

A gist with all the source code can be found here.