Как работать с асинхронными методами и IDisposable в C#?

У меня есть несколько интеграционных тестов с использованием 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
    }
}

Можете ли вы показать мне свои методы асинхронной утилизации? Никогда не слышал об интерфейсе, который асинхронно размещает себя. Вам обязательно нужно дождаться завершения асинхронного удаления, прежде чем класс будет удален? Финализаторы асинхронных объектов еще не реализованы. github.com/dotnet/coreclr/issues/22598

Avin Kavish 31.05.2019 08:56

@AvinKavish только что добавил пример

betabandido 31.05.2019 09:27

Я предполагаю, что логика удаления уникальна для метода тестирования? Вполне нормально перенести логику teardown в сам тест. Автор xUnit объясняет почему. jamesnewkirk.typepad.com/posts/2007/09/why-you-should-.html

Avin Kavish 31.05.2019 09:35

У вас действительно есть проблемы с использованием таких вещей, как .GetAwaiter().GetResult(), или вы пытаетесь предотвратить проблемы? Это не так уж плохо, если вы не работаете внутри контекста синхронизации (приложения пользовательского интерфейса...). Если вам не нужно «ждать» удаления, вы также можете запустить асинхронное выполнение и не ждать его завершения, например. async void - просто убедитесь, что не вызываете необработанных исключений, добавив try/catch

Martin Ullrich 31.05.2019 09:40

Кроме того, отправьте заявку на xunit для поддержки грядущего IAsyncDisposable в .net core 3.0, если вам это абсолютно необходимо.

Martin Ullrich 31.05.2019 09:40

@MartinUllrich Использование этих методов может привести к тупиковой ситуации при определенных обстоятельствах. Это довольно часто бывает при запуске тестов с помощью xunit.

betabandido 05.06.2019 21:14
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
6
1 540
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

Например:

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

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

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

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

Я просто надеялся, что можно будет избежать всего этого шаблонного кода. Я полагаю, что это на самом деле довольно аккуратно. Но я только что узнал, что xunit предоставляет некоторую поддержку для решения проблемы, с которой я столкнулся :)

betabandido 05.06.2019 21:19

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

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

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

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

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

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

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

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

Это означает, что ваш 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);
    }
}
}
Ответ принят как подходящий

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

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

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

Другие вопросы по теме