2016-10-17 3 views
5

я недавно пишу асинхронную метод, который вызывает внешний длинный ход асинхронная метод, поэтому я решил передать CancellationToken позволяющих отменить. Метод можно вызвать одновременно.Единицы Метод испытания асинхронный: Как явно утверждать, что внутренняя задача была отменена

Реализации объединила экспоненциальную отсрочку передачи и тайма-аута методов, описанных в Stephen Cleary «ы Книги Параллелизм в C# Поваренной следующим образом;

/// <summary> 
/// Sets bar 
/// </summary> 
/// <param name="cancellationToken">The cancellation token that cancels the operation</param> 
/// <returns>A <see cref="Task"/> representing the task of setting bar value</returns> 
/// <exception cref="OperationCanceledException">Is thrown when the task is cancelled via <paramref name="cancellationToken"/></exception> 
/// <exception cref="TimeoutException">Is thrown when unable to get bar value due to time out</exception> 
public async Task FooAsync(CancellationToken cancellationToken) 
{ 
    TimeSpan delay = TimeSpan.FromMilliseconds(250); 
    for (int i = 0; i < RetryLimit; i++) 
    { 
     if (i != 0) 
     { 
      await Task.Delay(delay, cancellationToken); 
      delay += delay; // Exponential backoff 
     } 

     await semaphoreSlim.WaitAsync(cancellationToken); // Critical section is introduced for long running operation to prevent race condition 

     using (CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) 
     { 
      cancellationTokenSource.CancelAfter(TimeSpan.FromMilliseconds(Timeout)); 
      CancellationToken linkedCancellationToken = cancellationTokenSource.Token; 

      try 
      { 
       cancellationToken.ThrowIfCancellationRequested(); 
       bar = await barService.GetBarAsync(barId, linkedCancellationToken).ConfigureAwait(false); 

       break; 
      } 
      catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) 
      { 
       if (i == RetryLimit - 1) 
       { 
        throw new TimeoutException("Unable to get bar, operation timed out!"); 
       } 

       // Otherwise, exception is ignored. Will give it another try 
      } 
      finally 
      { 
       semaphoreSlim.Release(); 
      } 
     } 
    } 
} 

Интересно, если я должен написать модульный тест, который явно утверждает, что внутренняя задача barService.GetBarAsync() отменяется, когда FooAsync() отменяется. Если да, то как его реализовать чисто?

Кроме того, следует ли игнорировать детали реализации и просто проверять, что такое клиент/вызывающий, как описано в сводке методов (строка обновлена, триггеры отмены OperationCanceledException, таймеры тайм-аута TimeoutException).

Если нет, то я должен получить мои ноги мокрые и приступить к реализации модульных тестов для следующих случаев:

  1. Тестирование является поточно-(монитор приобретается только по одной нити в то время)
  2. Тестирование повторить механизм
  3. Тестирование сервера не затопило
  4. Тестирование может быть, даже регулярное исключение передается вызывающему абоненту
+2

Это больше касается философии тестирования, поэтому, вероятно, нет окончательного ответа, но я склоняюсь к тестированию только открытого интерфейса. BarService - это собственный тип, поэтому вы должны проверить это своими собственными тестами, а не с этим. Тестирование комбинации, как вы предлагаете, скорее проверяет механизм отмены CLR, чем ваш код, и мы можем предположить, что все в порядке. Если, как я думаю, вы подразумеваете, что BarService является внутренним, вы можете использовать InternalsVisibleTo (только для сборки тестов), чтобы его можно было тестировать (но некоторые наверняка не согласятся). – sellotape

+0

@sellotape Я тестирую 'FooAsync()' на своем собственном приспособлении (Foo) _mocking_ 'BarService.GetBarAsync()'. Тем не менее, он реагирует на поведение зависимостей, и это то, что я рассматриваю для тестирования. Так что в основном; если вы подразумеваете, что я должен только проверять общее поведение, а не детали реализации, вы думаете, что я должен проверить на _thread safety_, учитывая, что я заявляю, что метод является потокобезопасным. –

+1

Если вы издеваетесь над этим, то нет ничего действительно проверить _ специально для него, и вы можете протестировать его изолированно. Re потоковой безопасности, да, вы, вероятно, должны проверить этот аспект, если это часть публичного контракта. Я оставлю вам более тонкие детали, но первоначальная мысль состоит в том, чтобы утверждать, что GetBarAsync() вызывается только до определенного момента времени, когда FooAsync() вызывается из 2 потоков примерно в одно и то же время. Вы можете настроить метод GetBarAsync() (mocked), чтобы немного задержаться, чтобы избежать слишком большой гонки. – sellotape

ответ

2

Интересно, следует ли мне написать единичный тест, который явно утверждает, что внутренняя задача barService.GetBarAsync() отменяется всякий раз, когда FooAsync() отменяется.

Было бы легче написать тест, который утверждает, что отмены маркера передается GetBarAsync отменяется, когда отмена маркер передается FooAsync отменяются.

Для асинхронных модульных испытаний мой сигнал выбора - TaskCompletionSource<object> для асинхронных сигналов и ManualResetEvent для синхронных сигналов. Поскольку GetBarAsync асинхронный, я хотел бы использовать асинхронный один, например,

var cts = new CancellationTokenSource(); // passed into FooAsync 
var getBarAsyncReady = new TaskCompletionSource<object>(); 
var getBarAsyncContinue = new TaskCompletionSource<object>(); 
bool triggered = false; 
[inject] GetBarAsync = async (barId, cancellationToken) => 
{ 
    getBarAsyncReady.SetResult(null); 
    await getBarAsyncContinue.Task; 
    triggered = cancellationToken.IsCancellationRequested; 
    cancellationToken.ThrowIfCancellationRequested(); 
}; 

var task = FooAsync(cts.Token); 
await getBarAsyncReady.Task; 
cts.Cancel(); 
getBarAsyncContinue.SetResult(null); 

Assert(triggered); 
Assert(task throws OperationCanceledException); 

Вы можете использовать сигналы, как это, чтобы создать своего рода «замок-шаг».


Сторона примечания: в моем собственном коде я никогда не пишу логику повтора. Я использую Polly, который полностью async-совместим и тщательно протестирован. Это позволит сократить семантику, которые должны быть проверены до:

  1. КТ проходит через (косвенно) к способу обслуживания, в результате чего OperationCanceledException при срабатывании.
  2. Существует также тайм-аут, в результате которого TimeoutException.
  3. Выполнение mutex'ed.

(1) будет выполнен точно так же, как указано выше. (2) и (3) менее просты в тестировании (для правильных тестов, требующих либо MS Fakes, либо абстракции для времени/мьютекса). Конечно, есть определенная точка снижения прибыли, когда дело доходит до модульного тестирования, и вам решать, как далеко вы хотите идти.

+0

Спасибо, что ответил Стивен. Мне нравится этот подход, поскольку я пытаюсь избежать неустойчивых/ненадежных решений Delay. Итак, снова с примерами, чтобы проверить, в этом конкретном примере, как далеко вы пойдете? –

+1

Это зависит от нескольких факторов. Я бы сделал (1) в любом случае и подумал о том, чтобы делать (2), если это был только я (команды обычно не имеют подделок). Я еще не дошел до (3). –

+1

[Тесты Polly для своей политики мьютексов] (https://github.com/App-vNext/Polly/blob/e9269ad9d24b9e0bd0cde546343d0c4d1539b77c/src/Polly.SharedSpecs/Bulkhead/BulkheadAsyncSpecs.cs#L94) (часть 3 в нумерации Стивена) по совпадению также продемонстрировать некоторые из методов тестирования, которые Стивен упоминает, - спросил Саро. [TaskCompletionSource ] (https://github.com/App-vNext/Polly/blob/e9269ad9d24b9e0bd0cde546343d0c4d1539b77c/src/Polly.SharedSpecs/Helpers/TraceableAction.cs#L18) используется как прокси-сервер, с помощью которого можно моделировать завершенную/отмененную асинхронную Работа. ... –

2

Спасибо Стивен Клири за кивок до Polly повторите попытку. Возможно, интерес к будущим читателям, все функции в образце кода оригинального плаката теперь может быть построен из готовых примитивов Polly, которые уже блок испытания:

  • Timeout policy для тайм-аута по тайм-ауту отмены маркера (включая объединение с поставляемым пользователем маркерами отмены)
  • Bulkhead policy для распараллеливания дроссельного/взаимного исключения
  • WaitAndRetry для повторной попытки, в том числе и во время отмены ждет
  • PolicyWrap для комбинирования.

Все политики Polly: fully unit-tested, синхронизация и асинхронная совместимость, поточно-безопасная для одновременных исполнений и поддержка сквозной отмены.

Так, намерение оригинального кода может быть достигнуто что-то вроде:

Policy retry = Policy.Handle<WhateverExceptions>().WaitAndRetryAsync(RetryLimit, retryAttempt => TimeSpan.FromMilliseconds(250 * Math.Pow(2, retryAttempt))); 
Policy mutex = Policy.BulkheadAsync(1); 
Policy timeout = Policy.TimeoutAsync(/* define overall timeout */); 

bar = await timeout.WrapAsync(retry).WrapAsync(mutex).ExecuteAsync(ct => barService.GetBarAsync(barId, ct), cancellationToken); 

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

+0

Спасибо за предоставленную вами информацию. Также большие пальцы руки для Полли, что облегчает жизнь в параллельном контексте (: –

Смежные вопросы