UPDATE: Jake Ginnivan pointed out an inefficiency in my code below. I was awaiting the async method under test twice.
As I mentioned a couple blog posts ago, I want to write tests that provide reasonable examples for users of my libraries. I expect developers to copy my tests as example code and modify it for production code.
That can be tough for async tests when the async method should throw an exception. Well, as I was busy laying the background, Phil Haack leaped ahead to the punchline in his recent blog post.
My own solution was a bit different:
public static async Task ThrowsExceptionAsync<TException>(Func<Task> func)
where TException : Exception
} catch (TException)
Assert.Fail(“Delegate did not throw “ + typeof(TException).Name);
And you’d use it like this:
await ThrowsExceptionAsync<ArgumentException>(() => worker.GetMessage(“ “));
Note that the worker.GetMessage() does not need to be awaited here, because it is awaited in the ThrowsExceptionAsync method.
Most of the differences between my version and Phil’s are due to the fact that I didn’t use any xUnit specific features. However, one part I will discuss in a bit more detail.
Note the ConfigureAwait(false) extension method when calling the async method under test: That instructs the framework to avoid the extra work of marshaling the continuation (the code after the await) onto the calling thread. This is a good practice for library code that does not need to have its continuations execute on the UI thread. I’ll discuss this in more detail in a future blog post.
There’s also a few big picture concepts to get across on this post. The first key practice is that that async methods should return Task, not be void methods. If your async methods are void returning, you can’t determine if your async methods worked correctly, or failed. By returning Task, you can query that Task (or let the compiler generate that code) to determine if it succeeded, failed, or was cancelled.
The second key is that you should await for the result of any async method that returns a Task or Task<T> (even in a lambda). That causes the compiler to generate all the code to check that returned Task and ensure that it completed successfully. If it completed in a faulted state, the compiler generates code to throw the exception during the continuation.
Finally, don’t Wait() for Tasks to finish. await tasks. The compiler generates much better code to handle continuations when you await. And, that code is asyncrhonous, not synchronous.