Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected behavior when passing async Actions around

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>.

like image 391
Daniel Mann Avatar asked Nov 03 '13 06:11

Daniel Mann


People also ask

Why you shouldn't use async void?

Async void methods can wreak havoc if the caller isn't expecting them to be async. When the return type is Task, the caller knows it's dealing with a future operation; when the return type is void, the caller might assume the method is complete by the time it returns.

Should all methods be async C#?

If a method has no async operations inside it there's no benefit in making it async . You should only have async methods where you have an async operation (I/O, DB, etc.). If your application has a lot of these I/O methods and they spread throughout your code base, that's not a bad thing.

Do you have to await an async function C#?

async methods need to have an await keyword in their body or they will never yield! This is important to keep in mind. If await is not used in the body of an async method, the C# compiler generates a warning, but the code compiles and runs as if it were a normal method.


1 Answers

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.

like image 86
noseratio Avatar answered Oct 14 '22 02:10

noseratio