Я пишу юнит-тесты для «связующего» слоя моего приложения, и мне трудно создавать детерминированные тесты для асинхронных методов, которые позволяют пользователю преждевременно отменить операцию.
В частности, в нескольких асинхронных методах у нас есть код, который реагирует на отмену вызова и гарантирует, что объект находится в надлежащем состоянии до завершения.Я хотел бы убедиться, что эти пути кода покрыты тестами.
Некоторый псевдокод C #, иллюстрирующий типичный асинхронный метод в этом сценарии, выглядит следующим образом:
public void FooAsync(CancellationToken token, Action<FooCompletedEventArgs> callback)
{
if (token.IsCancellationRequested) DoSomeCleanup0();
// Call the four helper methods, checking for cancellations in between each
Exception encounteredException;
try
{
MyDependency.DoExpensiveStuff1();
if (token.IsCancellationRequested) DoSomeCleanup1();
MyDependency.DoExpensiveStuff2();
if (token.IsCancellationRequested) DoSomeCleanup2();
MyDependency.DoExpensiveStuff3();
if (token.IsCancellationRequested) DoSomeCleanup3();
MyDependency.DoExpensiveStuff4();
if (token.IsCancellationRequested) DoSomeCleanup4();
}
catch (Exception e)
{
encounteredException = e;
}
if (!token.IsCancellationRequested)
{
var args = new FooCompletedEventArgs(a bunch of params);
callback(args);
}
}
Решение, которое у меня естьдо сих пор получается насмешка над основными MyDependency
операциями, которые обертываются клеевым слоем, и заставление каждой из них спать в течение произвольного периода времени.Затем я вызываю асинхронный метод и приказываю моему модульному тесту поспать несколько миллисекунд перед отменой асинхронного запроса.
Примерно так (на примере примера Rhino Mocks):
[TestMethod]
public void FooAsyncTest_CancelAfter2()
{
// arrange
var myDependency = MockRepository.GenerateStub<IMyDependency>();
// Set these stubs up to take a little bit of time each so we can orcestrate the cancels
myDependency.Stub(x => x.DoExpensiveStuff1()).WhenCalled(x => Thread.Sleep(100));
myDependency.Stub(x => x.DoExpensiveStuff2()).WhenCalled(x => Thread.Sleep(100));
myDependency.Stub(x => x.DoExpensiveStuff3()).WhenCalled(x => Thread.Sleep(100));
myDependency.Stub(x => x.DoExpensiveStuff4()).WhenCalled(x => Thread.Sleep(100));
// act
var target = new FooClass(myDependency);
CancellationTokenSource cts = new CancellationTokenSource();
bool wasCancelled = false;
target.FooAsync(
cts.Token,
args =>
{
wasCancelled = args.IsCancelled;
// Some other code to manipulate FooCompletedEventArgs
});
// sleep long enough for two operations to complete, then cancel
Thread.Sleep(250);
cts.Cancel();
// Some code to ensure the async call completes goes here
//assert
Assert.IsTrue(wasCancelled);
// Other assertions to validate state of target go here
}
Помимо того факта, что использование Thread.Sleep в модульном тесте вызывает у меня тошноту, большая проблема заключается в том, что иногда такие тесты не выполняются на нашем сервере сборки, если он оказывается под значительной нагрузкой.Асинхронный вызов заходит слишком далеко, а отмена приходит слишком поздно.
Может ли кто-нибудь предоставить более надежный способ логики отмены модульного тестирования для таких длительных операций?Любые идеи будут оценены.