Я пишу модульные тесты на слой "связующего звена" моего приложения и испытываю затруднения при создании детерминированных тестов для асинхронных методов, которые позволяют пользователю отменять операцию преждевременно.
А именно, в нескольких асинхронных методах у нас есть код, который реагирует на отмену вызова и гарантирует, что объект находится в надлежащем состоянии до завершения. Я хотел бы удостовериться, что эти пути выполнения кода покрыты тестами.
Некоторый псевдо код 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
операции, которые перенесены слоем связующего звена, и вынуждающий каждого спать в течение произвольного промежутка времени. Я затем вызываю асинхронный метод и говорю моему модульному тесту спать для многих миллисекунд прежде, чем отменить асинхронный запрос.
Что-то вроде этого (использование Насмешек Носорога как пример):
[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
}
Кроме факта тот Поток использования. Сон в модульном тесте делает меня тошнотворным, большая проблема состоит в том, что иногда тесты как этот сбой на нашем сервере сборки, если это, оказывается, является объектом значительной загрузки. Асинхронный вызов становится слишком далеким, и отмена происходит слишком поздно.
Кто-либо может обеспечить более надежный способ логики отмены поблочного тестирования для длительных операций как это? Любые идеи ценились бы.
Я бы попытался использовать имитацию для "симуляции" асинхронного поведения синхронным способом. Вместо того, чтобы использовать
myDependency.Stub(x => x.DoExpensiveStuff1()).WhenCalled(x => Thread.Sleep(100));
и затем устанавливать флаг отмены в течение любого количества миллисекунд, я бы просто установил его как часть обратного вызова:
myDependency.Stub(x => x.DoExpensiveStuff1());
myDependency.Stub(x => x.DoExpensiveStuff2());
myDependency.Stub(x => x.DoExpensiveStuff3()).WhenCalled(x => cts.Cancel());
myDependency.Stub(x => x.DoExpensiveStuff4());
с точки зрения вашего кода это будет выглядеть так, как если бы отмена произошла во время вызова.
Каждая из долго выполняющихся операций должна вызывать событие, когда она начинает выполняться.
Подключите это событие в модульном тесте. Это дает детерминированные результаты с возможностью того, что события могут быть полезны в будущем.