10

I'm pretty familiar with the async/await pattern, but I'm bumping into some behavior that strikes me as odd. I'm sure there's a perfectly valid reason why it's happening, and I'd love to understand the behavior.

The background here is that I'm developing a Windows Store app, and since I'm a cautious, conscientious developer, I'm unit testing everything. I discovered pretty quickly that the ExpectedExceptionAttribute doesn't exist for WSAs. Weird, right? Well, no problem! I can more-or-less replicate the behavior with an extension method! So I wrote this:

public static class TestHelpers
{
    // There's no ExpectedExceptionAttribute for Windows Store apps! Why must Microsoft make my life so hard?!
    public static void AssertThrowsExpectedException<T>(this Action a) where T : Exception
    {
        try
        {
            a();
        }
        catch (T)
        {
            return;
        }

        Assert.Fail("The expected exception was not thrown");
    }
}

And lo, it works beautifully.

So I continued happily writing my unit tests, until I hit an async method that I wanted to confirm throws an exception under certain circumstances. "No problem," I thought to myself, "I can just pass in an async lambda!"

So I wrote this test method:

[TestMethod]
public async Task Network_Interface_Being_Unavailable_Throws_Exception()
{
    var webManager = new FakeWebManager
    {
        IsNetworkAvailable = false
    };

    var am = new AuthenticationManager(webManager);
    Action authenticate = async () => await am.Authenticate("foo", "bar");
    authenticate.AssertThrowsExpectedException<LoginFailedException>();
}

This, surprisingly, throws a runtime error. It actually crashes the test-runner!

I made an overload of my AssertThrowsExpectedException method:

public static async Task AssertThrowsExpectedException<TException>(this Func<Task> a) where TException : Exception
{
    try
    {
        await a();
    }
    catch (TException)
    {
        return;
    }

    Assert.Fail("The expected exception was not thrown");
}

and I tweaked my test:

[TestMethod]
public async Task Network_Interface_Being_Unavailable_Throws_Exception()
{
    var webManager = new FakeWebManager
    {
        IsNetworkAvailable = false
    };

    var am = new AuthenticationManager(webManager);
    Func<Task> authenticate = async () => await am.Authenticate("foo", "bar");
    await authenticate.AssertThrowsExpectedException<LoginFailedException>();
}

I'm fine with my solution, I'm just wondering exactly why everything goes pear-shaped when I try to invoke the async Action. I'm guessing because, as far as the runtime is concerned, it's not an Action, I'm just cramming the lambda into it. I know the lambda will happily be assigned to either Action or Func<Task>.

Daniel Mann
  • 57,011
  • 13
  • 100
  • 120
  • 3
    In Windows Store tests, use `Assert.ThrowsException`, which ([as of VS2012 Update 2](http://support.microsoft.com/kb/2797912)) supports `async` lambdas. Note that `Action` is a *synchronous* method without a return value, while `Func` is an *asynchronous* method without a return value. – Stephen Cleary Nov 04 '13 at 03:33
  • Ooh, I didn't notice `Assert.ThrowsException`. I'll switch my tests over to use that. – Daniel Mann Nov 04 '13 at 14:29

1 Answers1

6

It is not surprising that it may crash the tester, in your second code fragment scenario:

Action authenticate = async () => await am.Authenticate("foo", "bar");
authenticate.AssertThrowsExpectedException<LoginFailedException>();

It's actually a fire-and-forget invocation of an async void method, when you call the action:

try
{
    a();
}

The a() returns instantly, and so does the AssertThrowsExpectedException method. At the same time, some activity started inside am.Authenticate may continue executing in the background, possibly on a pool thread. What's exactly going on there depends on the implementation of am.Authenticate, but it may crash your tester later, when such async operation is completed and it throws LoginFailedException. I'm not sure what is the synchronization context of the unit test execution environment, but if it uses the default SynchronizationContext, the exception may indeed be thrown unobserved on a different thread in this case.

VS2012 automatically supports asynchronous unit tests, as long as the test method signatures are async Task. So, I think you've answered your own question by using await and Func<T> for your test.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • You should also note that in your async method you are testing, you have to throw the error on the thread the method was called from or it won't be catchable by the caller and will still cause a runtime exception even with your solution. – Farrah Stark Nov 03 '13 at 20:15
  • @geezer498, I believe the OP's solution where he does `try { await a(); } catch ...` is correct. – noseratio Nov 03 '13 at 20:23
  • 1
    +1. FYI, MSTest uses the default (thread pool) task scheduler, so the exception is thrown on a thread pool thread, possibly crashing the test runner. I say "possibly" because technically there's a race condition between the test runner and the exception; e.g., if the exception was delayed then the test runner may actually complete before it crashed. – Stephen Cleary Nov 04 '13 at 03:31
  • Yeah, I think it's one of those "don't code at 1 am" things -- it seems a lot more obvious now. – Daniel Mann Nov 04 '13 at 14:28
  • @Noseratio, I'm not saying the solution is wrong. I'm saying that solution won't work if the Func throws the exception on a different Thread because that would mean the exception would be thrown out of scope of the try catch. Basically I'm saying be careful to code whatever ends up in the Func so it throws the error on a thread where it is catchable in the test. – Farrah Stark Nov 06 '13 at 02:54
  • @geezer498, an exception thrown inside `a()` may indeed happen on a different thread. However, if the calling code is awaiting on the result of `a()` like this: `try { await a(); } catch (e) { /* ... */ }`, the exception will be correctly propagated to the `catch` block, guaranteed. The method `a()` has to return a `Task` for this to be possible. The code after `await` may itself continue executing on a different thread, depending on the original thread's synchronization context or task scheduler. In the OP's case, that's what happens. – noseratio Nov 06 '13 at 03:26