2016-10-17 23 views
5

Byłem niedawno pisanie asynchronicznej metodę, która wywołuje zewnętrzny długi bieg asynchroniczny metoda więc postanowiłem przejść CancellationToken umożliwiającą anulowanie. Metodę można nazwać jednocześnie.Jednostka Metoda badania asynchroniczny: Jak wyraźnie stwierdzić, że zadanie zostało anulowane wewnętrzny

Wykonanie łączy odczekiwanie wykładniczym i limitu czasu technik opisanych w Stephen Cleary jest książka współbieżności C# Cookbook w następujący sposób;

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

Zastanawiam się, czy powinienem napisać badanej jednostki, która wyraźnie stwierdza, że ​​zadanie zostanie anulowane wewnętrzny barService.GetBarAsync() ilekroć FooAsync() zostanie anulowane. Jeśli tak, jak go wdrożyć w czysty sposób?

Czy powinienem zignorować szczegóły implementacji i po prostu przetestować, który klient/dzwoniący jest zainteresowany, jak opisano w podsumowaniu metody (pasek jest aktualizowany, anulować wyzwalacze OperationCanceledException, limit czasu wyzwala TimeoutException).

Jeśli nie, powinienem dostać moje nogi mokre i rozpoczęcie wdrażania testy jednostkowe w następujących przypadkach:

  1. Testowanie jest thread-safe (monitor nabyte tylko przez jeden wątek na raz)
  2. Testowanie ponowić mechanizm
  3. Testowanie serwer nie jest zalany
  4. Testowanie może nawet regularne wyjątek jest propagowana do dzwoniącego
+2

To jest więcej o testowaniu filozofii, więc prawdopodobnie nie ma jednoznacznej odpowiedzi, ale skłaniam się do testowania interfejsu publicznego. BarService jest własnym typem, więc powinieneś przetestować to z własnymi testami, a nie z tym. Testowanie kombinacji, jak sugerujesz, jest bardziej testem mechanizmu anulowania CLR niż kodu, i powinniśmy być w stanie założyć, że to w porządku. Jeśli - jak sądzę, sugerujesz - BarService jest wewnętrzna, możesz użyć InternalsVisibleTo (tylko dla zestawu testów), aby umożliwić testowanie (ale niektóre na pewno się nie zgodzą). – sellotape

+0

@ labelotape Testuję 'FooAsync()' na własnym urządzeniu (Foo) _mokowanie_ 'BarService.GetBarAsync()'. Jednak reaguje na zachowanie zależności i właśnie to rozważam na temat testowania. Więc zasadniczo; jeśli sugerujesz, że powinienem przetestować tylko ogólne zachowanie zamiast szczegółów implementacji, czy uważasz, że powinienem przetestować przeciwko _twanemu bezpieczeństwu_ biorąc pod uwagę, że twierdzę, że metoda jest bezpieczna dla wątków. –

+1

Jeśli kpisz z niego, to nie ma nic do przetestowania _do tego specjalnie_ i możesz przetestować go w izolacji. W związku z bezpieczeństwem, tak, prawdopodobnie musisz "przetestować ten aspekt, jeśli jest częścią kontraktu publicznego. Zostawię ci szczegóły, ale początkowo myślę, że GetBarAsync() jest wywoływana tylko raz w określonym momencie, kiedy FooAsync() jest wywoływana z 2 wątków mniej więcej w tym samym czasie. Możesz skonfigurować metodę GetBarAsync() (kpiącą), aby trochę opóźnić, aby uniknąć zbyt dużej ilości wyścigu. – sellotape

Odpowiedz

2

Zastanawiam się, czy powinienem napisać test jednostkowy, który wyraźnie potwierdza, że ​​wewnętrzne zadanie barService.GetBarAsync() jest anulowane, gdy funkcja FooAsync() jest anulowana.

Byłoby łatwiej napisać test, który twierdzi, że anulowanie żeton przekazane GetBarAsync jest anulowany, gdy token anulowanie przekazane FooAsync zostanie anulowane.

Do testowania urządzeń asynchronicznych moim wyborem jest TaskCompletionSource<object> dla sygnałów asynchronicznych i ManualResetEvent dla sygnałów synchronicznych. Od GetBarAsync jest asynchroniczne, użyłbym jednego asynchronicznego np

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

Można użyć sygnałów tak, aby stworzyć coś w rodzaju „lock-step”.


Nota boczna: w moim własnym kodzie nigdy nie zapisuję logiki ponownej próby. Używam Polly, który jest w pełni kompatybilny z async i dokładnie przetestowany. Które powodowałyby zmniejszenie semantyki, które muszą być badane w dół do:

  1. CT przechodzi przez (pośrednio) do metody usługi, w wyniku OperationCanceledException gdy uruchomiony.
  2. Istnieje również limit czasu, w wyniku czego TimeoutException.
  3. Wykonanie jest muteksowane.

(1) zostanie wykonane tak jak powyżej. (2) i (3) są mniej łatwe do przetestowania (do prawidłowych testów, wymagających albo MS Fakes lub abstrakcji na czas/muteks). Z pewnością chodzi o zmniejszenie zysków, jeśli chodzi o testowanie jednostek, a od Ciebie zależy, jak daleko chcesz się posunąć.

+0

Dzięki za odpowiedź na Stephena. Podoba mi się to podejście, ponieważ staram się unikać niestabilnych/zawodnych rozwiązań opóźniających. Tak więc, ponownie z przypadkami do przetestowania, w tym konkretnym przypadku, jak daleko byś poszła? –

+1

To zależy od kilku czynników. Zrobiłbym (1) w każdym przypadku i rozważę zrobienie (2), jeśli to tylko ja (zespoły zwykle nie mają dostępnych podróbek). Jeszcze nie posunąłem się tak daleko (3). –

+1

[Testy Polly dotyczące polityki muteksa] (https://github.com/App-vNext/Polly/blob/e9269ad9d24b9e0bd0cde546343d0c4d1539b77c/src/Polly.SharedSpecs/Bulkhead/BulkheadAsyncSpecs.cs#L94) (część 3 w numeracji Stephena) przypadkowo również zademonstrować niektóre z technik testowania, o których wspominał Stephen/Saro. [TaskCompletionSource ] (https://github.com/App-vNext/Polly/blob/e9269ad9d24b9e0bd0cde546343d0c4d1539b77c/src/Polly.SharedSpecs/Helpers/TraceableAction.cs#L18) służy jako proxy do symulowania ukończonych/anulowanych asynchronizacji praca. ... –

2

Podziękowania dla Stephena Cleary'ego za skinienie w kierunku Polly spróbuj ponownie. Być interesujące dla przyszłych czytelników, cała funkcjonalność oryginalnego plakatu Kod próbki mógłby teraz być budowane z gotowych prymitywów Polly, które są już jednostką przetestowane:

  • Timeout policy dla limitu czasu przez czas-out anulowania tokena (w tym połączenie z dostarczonego przez użytkownika anulowania token)
  • Bulkhead policy do zrównoleglenie dławienia/wzajemnego wykluczania
  • WaitAndRetry dla ponownego wprowadzenia, w tym anulowanie podczas czeka
  • PolicyWrap do połączenia.

Wszystkie zasady dotyczące polly są fully unit-tested, synchronizowane i zgodne z Async, bezpieczne dla wielu wątków w przypadku równoczesnych wykonań i mają funkcję anulowania tranzytu.

Więc intencją oryginalnego kodu można osiągnąć coś takiego:

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

dodam kilka uwag o jednostce testowania (oryginalne pytanie PO), aby komentarzach do Stefana (o wiele bardziej istotna) odpowiedź na to pytanie.

+0

Dziękuję za udostępnione informacje. Kciuki w górę dla Polly, które znacznie ułatwia życie w kontekście współbieżnym (: –