Hacking the Silverlight Unit Tests to support returning Task

The Silverlight/Windows Phone unit test framework has always supported running asynchronous tests – a feature that until recently wasn’t there in WPF without jumping some really ugly (and flaky) hoops. Basically you can write a silverlight and windows phone unit test like this:

[TestClass]
public class TestClass1 : SilverlightTest
{
    [TestMethod]
    [Asynchronous] 
public void Test1() { DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(2) }; timer.Tick += (a, b) => { timer.Stop(); base.TestComplete(); }; timer.Start(); } }

The problem with this code though is that this is only for Silverlight and Windows Phone. If you are cross-compiling for multiple platforms and want to run on WPF this wouldn’t work. It’s also not pretty that you have to inherit from SilverlightTest, remember to decorate the class with [Asynchronous] as well as calling TestComplete. Even worse, if you forget to stop the timer, it would CRASH the entire test run. The unit test framework is a little flaky when it comes to a task accidentally completing twice (instead of reporting it as an error, it crashes the entire test run and you’ll never get your daily test report…).

With Visual Studio 2012 and .NET 4.5 we can now simply return an object of type ‘Task’ and we would be good to go. This is awesome for testing your new async/await based stuff that returns task. So in WPF you would simply return your task object. As an example, let’s say we have the following really advanced computing task:

public static Task<int> Compute(int input)
{
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(2) };
    timer.Tick += (a, b) =>
    {
        timer.Stop();
        if (input <= 0)
            tcs.SetException(new ArgumentOutOfRangeException("Number must be greater than 0"));
        else 
            tcs.SetResult(input);
    };
    timer.Start();
    return tcs.Task;
}

Now to test this in .NET 4.5 (including Windows Store Apps) you can simply write the following unit test:

[TestClass] 
public class TestClass1
{
[TestMethod] public async Task Test42() { var result = await Utility.Compute(42); Assert.AreEqual(result, 42); }
}

Nice and simple. However in Silverlight and Windows Phone you would have to write the following instead (I highlighted the extra or changed code required):

[TestClass]
public class TestClass1 : SilverlightTest
{
    [TestMethod]
    [Asynchronous]
    public async void Test42()
    {
        var result = await Utility.Compute(42);
        Assert.AreEqual(result, 42);
        base.TestComplete();
    }
}

Wouldn’t it be nice if the unit test I just wrote for WPF would work as is in Silverlight and on Windows Phone? Of course you could create a SilverlightTest class that has an empty TestComplete method, define an AsynchronousAttribute just for fun, and sprinkle a compiler conditional around the void/Task return type, but that just feels messy to me.

Fortunately the unit test framework for Silverlight is open source, so it’s possible to hack it in there. There are two main places you will need to change, which I will go through here. Note this is based on changeset #80285.

In the file “\Microsoft.Silverlight.Testing\UnitTesting\UnitTestMethodContainer.cs” we add the highlighted code to the method that detects if the Asynchronous attribute is on a method:

private bool SupportsWorkItemQueue()
{
    if (_testMethod != null)
    {
        if (_testMethod.Method.ReturnType != null && 
            _testMethod.Method.ReturnType == typeof(System.Threading.Tasks.Task) ||
            _testMethod.Method.ReturnType.IsSubclassOf(typeof(System.Threading.Tasks.Task)))
            return true; //Task Support
        else
            return ReflectionUtility.HasAttribute(_testMethod, typeof(AsynchronousAttribute));
    }
    else if (MethodInfo != null)
    {
        return ReflectionUtility.HasAttribute(MethodInfo, typeof(AsynchronousAttribute));
    }
    else
    {
        return false;
    }
}

Next is modifying the Invoke method that executes your test, which is located in ‘Microsoft.Silverlight.Testing\Metadata\VisualStudio\TestMethod.cs’. This is where the main work is done to enable tasks to work:

public virtual void Invoke(object instance)
{
    _methodInfo.Invoke(instance, None);
}

This now changes to:

public virtual void Invoke(object instance, CompositeWorkItem workItem)
{
    var t = _methodInfo.Invoke(instance, None) as System.Threading.Tasks.Task;
    if (t != null)
    {
        if (t.IsFaulted)
        {
            throw t.Exception;
        }
        else if (!t.IsCompleted)
        {
            var context = System.Threading.SynchronizationContext.Current;
            t.ContinueWith(result =>
            {
                context.Post((d) =>
                {
                    if (result.IsFaulted)
                    {
                        Exception ex = result.Exception;
                        if (ex is AggregateException)
                            ex = ex.GetBaseException();
                        workItem.WorkItemException(ex);
                    }
                    else
                        workItem.WorkItemCompleteInternal();
                }, null);
            });
        }
    }
}

Basically it grabs the task that is returned and calls the code that TestComplete would have called or what a raised exception would have called in case the test raises an exception. Also note that we changed the signature of the method to give us the CompositeWorkItem we need to raise these events on. This change does affect quite a lot of other code, but it’s merely a matter of adding the same parameter there as well, and the only place that calls this method (which is the CompositeWorkItem) to set this parameter to ‘this’.

Now you can also write tests that tests for exceptions thrown. Often you don’t even need to await the result in those cases:

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public Task TestOutOfRange()
{
    return Utility.Compute(0);  //no need to await
}

[TestMethod]
public Task TestOutOfRange_Failure() //This test will fail
{
    return Utility.Compute(0);
}

And here’s what that looks like for the entire test run:

image

To make it easy on you, you can download the modified unit test framework source here.

…But EVEN better: Go vote for this to be part of the official toolkit here:  http://silverlight.codeplex.com/workitem/11457

Pingbacks and trackbacks (1)+

Add comment