Как бороться с асинхронными методами и IDisposable в C #? - PullRequest
3 голосов
/ 31 мая 2019

У меня есть несколько интеграционных тестов, использующих xUnit, которые должны разрушить некоторые ресурсы, созданные во время теста.Для этого я внедрил IDisposable в классе, содержащем тесты.

Проблема в том, что мне нужно удалить ресурсы, созданные во время теста, с помощью клиента, который имеет только асинхронный интерфейс.Но метод Dispose является синхронным.

Я мог бы использовать .Result или .Wait() для ожидания завершения асинхронного вызова, но это может привести к взаимоблокировке (проблема хорошо документирована здесь ).

Учитывая, что я не могу использовать .Result или .Wait(), каков правильный (и безопасный) способ вызова асинхронного метода в методе Dispose?

ОБНОВЛЕНИЕ: добавление (упрощенного) примера, чтобы показать проблему.

[Collection("IntegrationTests")]
public class SomeIntegrationTests : IDisposable {
    private readonly IClient _client; // SDK client for external API

    public SomeIntegrationTests() {
        // initialize client
    }

    [Fact]
    public async Task Test1() {
        await _client
            .ExecuteAsync(/* a request that creates resources */);

        // some assertions
    }

    public void Dispose() {
        _client
            .ExecuteAsync(/* a request to delete previously created resources */)
            .Wait(); // this may create a deadlock
    }
}

Ответы [ 3 ]

3 голосов
/ 31 мая 2019

У меня похожие проблемы, особенно XUnit - это проблемный ребенок здесь.Я «решил» это с перемещением всего кода очистки в тест, например, блок try..finally.Это менее элегантно, но работает более стабильно и позволяет избежать асинхронного удаления.Если у вас есть много тестов, вы можете добавить метод, который уменьшает шаблон.

Например:

        private async Task WithFinalizer(Action<Task> toExecute)
    {

        try
        {
            await toExecute();
        }
        finally
        {
           // cleanup here
        }
    }

    // Usage
    [Fact]
    public async Task TestIt()
    {
        await WithFinalizer(async =>
        {
         // your test
         });
    }

Другое преимущество этого заключается в том, что, по моему опыту, очистка частов значительной степени зависит от теста - с помощью этого метода гораздо проще создать собственный финализатор для каждого теста (добавить второе действие, которое можно использовать в качестве финализатора)

1 голос
/ 05 июня 2019

Оказывается, xunit действительно имеет некоторую поддержку для решения проблемы, с которой я столкнулся. Классы тестирования могут реализовывать IAsyncLifetime для инициализации и разрыва тестов асинхронным способом. Интерфейс выглядит так:

public interface IAsyncLifetime
{
    Task InitializeAsync();
    Task DisposeAsync();
}

Хотя это решение моей конкретной проблемы, оно не решает более общую проблему вызова асинхронного метода из Dispose (ни один из текущих ответов также не делает этого). Полагаю, для этого нам нужно подождать, пока IAsyncDisposable не будет доступен в .NET core 3.0 (спасибо @MartinUllrich за эту информацию).

0 голосов
/ 31 мая 2019

Тестовый класс выполняет несколько тестов, которые связаны друг с другом. Тестовый класс обычно тестирует класс или группу классов, которые тесно сотрудничают. Иногда тестовый класс проверяет только одну функцию.

Обычно тесты должны быть спроектированы таким образом, чтобы они не зависели от других тестов: тест А должен пройти успешно без запуска теста Б, и наоборот: тесты могут не предполагать ничего о других тестах.

Обычно тест создает некоторое предварительное условие, вызывает тестируемую функцию и проверяет, выполняется ли постусловие. Поэтому каждый тест обычно создает свою собственную среду.

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

Однако, если в одном из ваших тестов вы создаете задачу для вызова асинхронной функции, вам следует подождать в этом тесте результата этой задачи. Если вы этого не сделаете, вы не сможете проверить, выполняет ли асинхронная функция то, для чего она предназначена, а именно: «Создать задачу, которая, когда ожидается, возвращает ...».

void TestA()
{
    Task taskA = null;
    try
    {
        // start a task without awaiting
        taskA = DoSomethingAsync();
        // perform your test
        ...
        // wait until taskA completes
        taskA.Wait();
        // check the result of taskA
        ...
     }
     catch (Exception exc)
     {
         ...
     }
     finally
     {
         // make sure that even if exception TaskA completes
         taskA.Wait();
     }
 }

Вывод: каждый метод Test, создающий задачу, должен дождаться завершения этого класса до завершения

В редких случаях вам не нужно ждать, пока задание завершится, до завершения теста. Может быть, посмотреть, что произойдет, если вы не дождетесь этой задачи. Я все еще думаю, что это странная идея, потому что это может повлиять на другие тесты, но, эй, это ваш тестовый класс.

Это означает, что ваш Dispose должен удостовериться, что ожидаются все запущенные задачи, которые не были завершены к моменту окончания метода тестирования.

List<Task> nonAwaitedTasks = new List<Task>();

var TestA()
{
    // start a Task, for some reason you don't want to await for it:
    Task taskA = DoSomethingAsync(...);
    // perform your test

    // finish without awaiting for taskA. Make sure it will be awaited before the
    // class is disposed:
    nonAwaitedTasks.Add(taskA);
}

public void Dispose()
{
    Dispose(true);
}
protected void Dispose(bool disposing)
{
    if (disposing)
    {
        // wait for all tasks to complete
        Task.WaitAll(this.nonAwaitedTasks);
    }
}
}
...