У меня есть несколько интеграционных тестов с использованием 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
}
}
@AvinKavish только что добавил пример
Я предполагаю, что логика удаления уникальна для метода тестирования? Вполне нормально перенести логику teardown
в сам тест. Автор xUnit объясняет почему. jamesnewkirk.typepad.com/posts/2007/09/why-you-should-.html
У вас действительно есть проблемы с использованием таких вещей, как .GetAwaiter().GetResult()
, или вы пытаетесь предотвратить проблемы? Это не так уж плохо, если вы не работаете внутри контекста синхронизации (приложения пользовательского интерфейса...). Если вам не нужно «ждать» удаления, вы также можете запустить асинхронное выполнение и не ждать его завершения, например. async void
- просто убедитесь, что не вызываете необработанных исключений, добавив try/catch
Кроме того, отправьте заявку на xunit для поддержки грядущего IAsyncDisposable
в .net core 3.0, если вам это абсолютно необходимо.
@MartinUllrich Использование этих методов может привести к тупиковой ситуации при определенных обстоятельствах. Это довольно часто бывает при запуске тестов с помощью xunit.
У меня похожие проблемы, особенно 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 предоставляет некоторую поддержку для решения проблемы, с которой я столкнулся :)
Тестовый класс выполняет несколько тестов, которые связаны друг с другом. Тестовый класс обычно проверяет класс или группу классов, которые тесно взаимодействуют друг с другом. Иногда тестовый класс проверяет только одну функцию.
Обычно тесты должны быть разработаны таким образом, чтобы они не зависели от других тестов: тест А должен пройти без запуска теста Б, и наоборот: тесты могут ничего не предполагать о других тестах.
Обычно тест создает некоторое предусловие, вызывает тестируемую функцию и проверяет, выполняется ли постусловие. Поэтому каждый тест обычно создает свою собственную среду.
Если для набора тестов требуется одинаковая среда, то для экономии времени тестирования довольно часто среду создают один раз для всех этих тестов, запускают тесты и удаляют среду. Это то, что вы делаете в своем тестовом классе.
Однако, если в одном из ваших тестов вы создаете задачу для вызова асинхронной функции, вам следует дождаться результата этой задачи внутри этого теста. Если вы этого не сделаете, вы не сможете проверить, делает ли асинхронная функция то, для чего она предназначена, а именно: «Создайте задачу, которая при ожидании возвращает…».
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 за эту информацию).
Можете ли вы показать мне свои методы асинхронной утилизации? Никогда не слышал об интерфейсе, который асинхронно размещает себя. Вам обязательно нужно дождаться завершения асинхронного удаления, прежде чем класс будет удален? Финализаторы асинхронных объектов еще не реализованы. github.com/dotnet/coreclr/issues/22598