Неожиданное поведение при передаче async Actions around

Я довольно хорошо знаком с шаблоном async/await, но я натыкаюсь на какое-то поведение, которое поражает меня как странное. Я уверен, что существует вполне обоснованная причина, почему это происходит, и я хотел бы понять поведение.

Фон здесь заключается в том, что я разрабатываю приложение для Windows Store, и, поскольку я осторожный, добросовестный разработчик, я все тестирую. Я довольно быстро обнаружил, что ExpectedExceptionAttribute не существует для WSA. Странно, правда? Ну, не проблема! Я могу больше или меньше реплицировать поведение с помощью метода расширения! Поэтому я написал следующее:

public static class TestHelpers
{
    // There 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");
    }
}

И вот, это прекрасно работает.

Итак, я продолжал счастливо записывать свои модульные тесты, пока не ударил метод асинхронной проверки, который я хотел бы подтвердить, вызывая исключение при определенных обстоятельствах. "Нет проблем, - подумал я про себя, - я могу просто пройти асинхронную лямбду!"

Итак, я написал этот тестовый метод:

[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>();
}

Это, что удивительно, вызывает ошибку времени выполнения. Это на самом деле сбивает тест-бегуна!

Я сделал перегрузку моего метода AssertThrowsExpectedException:

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");
}

и я изменил свой тест:

[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>();
}

Я в порядке с моим решением, мне просто интересно, почему все идет грушевидно, когда я пытаюсь вызвать async Action. Я угадываю, потому что, насколько касается времени выполнения, это не Action, я просто забиваю лямбду в нее. Я знаю, что лямбда с радостью будет назначена либо Action, либо Func<Task>.

Ответ 1

Неудивительно, что это может привести к сбою тестера во втором сценарии фрагмента кода:

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

На самом деле это вызов fire-and-forget async void при вызове действия:

try
{
    a();
}

a() возвращает мгновенно, а также метод AssertThrowsExpectedException. В то же время, некоторая активность, начинающаяся внутри am.Authenticate, может продолжаться в фоновом режиме, возможно, в потоке пула. То, что происходит там, зависит от реализации am.Authenticate, но позже может произойти сбой вашего тестера, когда будет завершена такая операция aync и будет выбрана LoginFailedException. Я не уверен, что представляет собой контекст синхронизации среды выполнения unit test, но если он использует значение по умолчанию SynchronizationContext, в этом случае исключение может быть брошено ненаблюдаемым в другом потоке.

VS2012 автоматически поддерживает асинхронные модульные тесты, если сигнатуры метода тестирования async Task. Итак, я думаю, вы ответили на свой вопрос, используя await и Func<T> для своего теста.