Il presente articolo è stato tradotto automaticamente.

Programmazione asincrona

Codice asincrono degli unit test

Stephen Cleary

Scaricare il codice di esempio

Unit test è una pietra angolare dello sviluppo moderno. I vantaggi di unit test per un progetto sono capiti abbastanza bene: Unit test diminuisce il numero di bug, riduce il tempo al mercato e scoraggia la progettazione eccessivamente accoppiati. Quelli sono benefici tutto bello, ma ci sono ulteriori vantaggi più direttamente rilevanti per gli sviluppatori. Quando scrivo unit test, posso avere una maggiore fiducia nel codice. È più facile aggiungere funzionalità o correggere i bug nel codice testato, perché l'unità test atto come una rete di sicurezza, mentre il codice sta cambiando.

Scrittura di unit test per codice asincrono porta alcune sfide uniche. Inoltre, lo stato corrente di async supporto nella prova di unità e beffardo quadri varia ed è ancora in evoluzione. Questo articolo prenderà in considerazione MSTest, NUnit xUnit, ma i principi generali si applicano a qualsiasi unit test framework. La maggior parte degli esempi in questo articolo verrà utilizzato MSTest sintassi, ma ti segnalo eventuali differenze di comportamento lungo la strada. Download del codice contiene esempi per tutti i tre quadri.

Prima di tuffarsi nello specifico, esamineremo brevemente un modello concettuale di come async e attendono lavoro Parole chiavi.

Async e attendono in breve

La parola chiave async fa due cose: Esso consente la parola chiave await all'interno di tale metodo, e trasforma il metodo in una macchina a stati (simile a come la parola chiave yield trasforma blocchi iteratore in macchine di stato). I metodi asincroni devono restituire Task o Task < T > Quando possibile. È ammissibile per un metodo asincrono a restituire void, ma non è consigliabile perché è molto difficile da consumare (o test) un metodo void async.

L'istanza di attività restituito da un metodo asincrono è gestito dalla macchina dello stato. La macchina dello stato verrà creata l'istanza di attività per restituire e successivamente completerà quel compito.

Un metodo asincrono inizia l'esecuzione in modo sincrono. È solo quando il metodo async raggiunge un operatore di attendere che il metodo può diventare asincrono. L'operatore di attesa accetta un solo argomento, un "awaitable" come un'istanza dell'attività. In primo luogo, l'operatore di attesa controllerà il awaitable per vedere se è già stata completata; Se ha, il metodo continua (modo sincrono). Se il awaitable non è ancora completo, l'operatore di attesa sarà "in pausa" il metodo e riprendere quando completa il awaitable. La seconda cosa che fa l'operatore attendere è recuperare eventuali risultati dalle awaitable, solleva eccezioni se completato il awaitable con un errore.

L'attività o < T > restituito da async metodo concettualmente rappresenta l'esecuzione di tale metodo. L'attività verrà completata quando il metodo viene completata. Se il metodo restituisce un valore, il compito è completato con tale valore come risultato. Se il metodo genera un'eccezione (e non prenderlo), il compito è completato con tale eccezione.

Ci sono due lezioni immediati trarre da questa breve panoramica. In primo luogo, durante il test i risultati di un metodo asincrono, il bit importante è il compito che restituisce. Il metodo async utilizza il suo compito di segnalare il completamento, risultati ed eccezioni. La seconda lezione è che l'operatore di attesa ha un comportamento speciale quando sua awaitable è già completa. Tratterò questo più tardi quando si considerano gli stub asincroni.

L'erroneamente passando Unit Test

Nell'economia di mercato, le perdite sono importanti tanto quanto i profitti; e ' i fallimenti delle aziende che li costringono a produrre ciò che la gente acquista e incoraggiare l'allocazione ottimale delle risorse all'interno del sistema intero. Analogamente, i fallimenti di unit test sono importanti tanto quanto i loro successi. Si deve essere sicuri che lo unit test avrà esito negativo quando dovrebbe, o il suo successo non significa nulla.

Uno unit test che doveva per fallire riuscirà (erroneamente) quando sta testando la cosa sbagliata. Ecco perché il test driven development (TDD) fa un uso pesante del ciclo rosso/verde/refactoring: la parte "rossa" del ciclo assicura che lo unit test avrà esito negativo quando il codice è errato. Al primo, test codice che sai essere sbagliato suona ridicolo, ma in realtà è abbastanza importante perché si deve essere sicuri i test avrà esito negativo quando hanno bisogno di. La parte rossa del ciclo TDD è effettivamente testando le prove.

Con questo in mente, si consideri il seguente metodo asincrono per testare:

public sealed class SystemUnderTest
{
  public static async Task SimpleAsync()
  {
    await Task.Delay(10);
  }
}

I nuovi arrivati a async unit test spesso farà un test come questo come un primo tentativo:

// Warning: bad code!
[TestMethod]
public void IncorrectlyPassingTest()
{
  SystemUnderTest.SimpleAsync();
}

Purtroppo, questo unit test in realtà non testare correttamente il metodo asincrono. Se modifico il codice sotto test a fallire, passerà ancora lo unit test:

public sealed class SystemUnderTest
{
  public static async Task SimpleAsync()
  {
    await Task.Delay(10);
    throw new Exception("Should fail.");
  }
}

Ciò illustra la prima lezione dal modello concettuale async/attendono: Per testare il comportamento del metodo asincrono, deve osservare l'attività che restituisce. Il modo migliore per farlo è quello di attendere il task restituito dal metodo da testare. Questo esempio illustra anche il beneficio di rosso/verde/refactor test ciclo di sviluppo; è necessario assicurarsi che il test avrà esito negativo quando il codice sotto test ha esito negativo.

Framework di test di unità più moderne supportano attività restituendo asincrona unit test. Il metodo IncorrectlyPassingTest provocherà CS4014, che consiglia di utilizzare attesa a consumare l'attività restituita da SimpleAsync di avviso del compilatore. Quando il metodo di unit test è cambiato per attendere il task, l'approccio più naturale è cambiare il metodo di prova per essere una metodo di attività asincrone. Questo assicura che il metodo di prova non riuscirà (correttamente):

[TestMethod]
public async Task CorrectlyFailingTest()
{
  await SystemUnderTest.FailAsync();
}

Evitando Async Void Unit test

Gli utenti esperti di async sappiano evitare async vuoto. Descritto i problemi con async nulle nel mio articolo di marzo 2013, "Best Practices in programmazione asincrona" (bit.ly/1ulDCiI). Metodi di Async void unit test non forniscono un modo semplice per loro framework di unit test recuperare i risultati del test. Nonostante questa difficoltà, alcuni framework di test di unità supportano async void unit test fornendo loro SynchronizationContext nella quale vengono eseguiti i test di unità.

Fornire un SynchronizationContext è alquanto controversa, perché modifica l'ambiente in cui vengono eseguiti i test. In particolare, quando un metodo asincrono attende un compito, per impostazione predefinita riprenderà quel metodo async sull'oggetto SynchronizationContext corrente. Così la presenza o l'assenza di un SynchronizationContext indirettamente cambierà il comportamento del sistema in esame. Se siete curiosi di sapere i dettagli di SynchronizationContext, vedi il mio articolo di MSDN Magazine sul tema a bit.ly/1hIar1p.

MSTest non fornisce un SynchronizationContext. Infatti, quando MSBuild è scoprendo il test in un progetto che utilizza async void unit test, rileva questo e problema attenzione UTA007, avvisare l'utente che l'unità metodo di prova deve restituire il compito invece di vuoto. Async void unit test non viene eseguito MSBuild .

NUnit supporto asincrono void unit test, a partire dalla versione 2.6.2. Il prossimo aggiornamento importante di NUnit, versione 2.9.6, supporta async void unit test, ma gli sviluppatori hanno già deciso di rimuovere il supporto in versione 2.9.7. NUnit prevede un SynchronizationContext solo async void unit test.

A partire da questa scrittura, xUnit sta progettando di aggiungere il supporto per async void unit test con la versione 2.0.0. A differenza di NUnit, xUnit fornisce un SynchronizationContext per tutti i metodi di prova, anche quelli sincroni. Tuttavia, con MSTest non supportante async void unit test e con NUnit invertendo la sua precedente decisione e la rimozione di supporto, non sarei sorpreso se xUnit sceglie anche a goccia async void unità supporto di prova prima la versione 2 è stato rilasciato.

La linea di fondo è che async void unit test sono complicate per i quadri di sostegno, richiedono cambiamenti nell'ambiente di esecuzione test e non portare alcun beneficio sopra async unit test del compito. Inoltre, il supporto asincrono void unit test varia tra quadri e anche versioni di framework. Per questi motivi, è meglio evitare di async void unit test.

Async Task Unit test

Async unit test che restituiscono il compito avere nessuno dei problemi di async unit test che restituiscono void. Async unit test che restituiscono il compito godere di ampio sostegno da quasi tutti i framework di test di unità. MSTest aggiunto supporto in Visual Studio 2012, NUnit in versione 2.6.2 e 2.9.6 e xUnit nella versione 1.9. Quindi, fintanto che il framework di test di unità è inferiore a 3 anni di età, dovrebbe funzionare appena async task unit test.

Purtroppo, il framework di test di unità obsolete non capisco async task unit test. Al momento, c'è una grande piattaforma che non li supporta: Novell. Novell utilizza una versione personalizzata di più vecchia di NUnitLite, e attualmente non supporta async task unit test. Mi aspetto che supporto verrà aggiunti nel prossimo futuro. Nel frattempo, io uso una soluzione alternativa che è inefficiente, ma funziona: Eseguire la logica di test async su un pool di thread differenti e quindi bloccare il metodo di unit test (modo sincrono) fino al completamento della prova effettiva. Il codice della soluzione utilizza GetAwaiter().GetResult() invece di aspettare perché attesa vi avvolgerà tutte le eccezioni all'interno di un AggregateException:

[Test]
public void XamarinExampleTest()
{
  // This workaround is necessary on Xamarin,
  // which doesn't support async unit test methods.
  Task.Run(async () =>
  {
    // Actual test code here.
  }).GetAwaiter().GetResult();
}

Testare le eccezioni

Durante il test, è naturale per testare lo scenario di successo; ad esempio, un utente può aggiornare il proprio profilo. Tuttavia, test di eccezioni è anche molto importante; ad esempio, un utente non dovrebbe essere in grado di aggiornare il profilo di qualcun altro. Le eccezioni sono parte di un'area di API così come sono i parametri del metodo. Pertanto, è importante avere unit test per codice quando ci si aspetta di fallire.

Originariamente, il ExpectedExceptionAttribute è stato posto su un metodo di unit test per indicare che il test di unità è stato previsto a fallire. Tuttavia, ci sono stati alcuni problemi con ExpectedException­attributo. Il primo era che essa poteva aspettarsi solo uno unit test a fallire nel suo complesso; non non c'era alcun modo per indicare che solo una determinata parte del test è stato previsto a fallire. Questo non è un problema con test molto semplice, ma può avere risultati fuorvianti quando i test crescono più a lungo. Il secondo problema con ExpectedExceptionAttribute è che esso di limitata alla verifica del tipo di eccezione; c'è modo di controllare gli altri attributi, ad esempio codici di errore o messaggi.

Per questi motivi, negli ultimi anni c'è stato uno spostamento verso qualcosa di più come Assert.ThrowsException, che prende la parte importante del codice come delegato e restituisce l'eccezione che è stata generata utilizzando. Questo risolve le carenze di ExpectedExceptionAttribute. Il framework MSTest desktop supporta solo ExpectedExceptionAttribute, mentre il più recente quadro MSTest utilizzato per progetti di unit test di Windows Store supporta solo Assert.ThrowsException. xUnit supporta solo Assert.Throws e NUnit supporta entrambi gli approcci. Figura 1 è un esempio di entrambi i tipi di test, utilizzando la sintassi MSTest.

Figura 1 testare le eccezioni con metodi di prova sincrono

// Old style; only works on desktop.
[TestMethod]
[ExpectedException(typeof(Exception))]
public void ExampleExpectedExceptionTest()
{
  SystemUnderTest.Fail();
}
// New style; only works on Windows Store.
[TestMethod]
public void ExampleThrowsExceptionTest()
{
  var ex = Assert.ThrowsException<Exception>(() 
    => { SystemUnderTest.Fail(); });
}

Ma per quanto riguarda il codice asincrono? Async task unit test lavoro perfettamente bene con ExpectedExceptionAttribute MSTest sia NUnit (xUnit non supporta ExpectedExceptionAttribute affatto). Tuttavia, il supporto per un ThrowsException di async-ready è meno uniforme. MSTest supportano un async ThrowsException, ma solo per unità Windows Store testare progetti. xUnit ha introdotto un ThrowsAsync async in compilazioni prerelease di xUnit 2.0.0.

NUnit è più complessa. A partire da questa scrittura, NUnit supporta il codice asincrono nei suoi metodi di verifica come Assert.Throws. Tuttavia, al fine di ottenere questo lavoro, NUnit fornisce un SynchronizationContext, che presenta gli stessi problemi di async void unit test. Inoltre, la sintassi è attualmente fragile, come l'esempio in Figura 2 illustrato. NUnit è già in programma di abbandonare il supporto per async void unit test, e non mi stupirei se questo supporto è sceso allo stesso tempo. In sintesi: Vi consiglio di che non utilizzare questo approccio.

NUnit fragile figura 2 test di eccezione

[Test]
public void FailureTest_AssertThrows()
{
  // This works, though it actually implements a nested loop,
  // synchronously blocking the Assert.Throws call until the asynchronous
  // FailAsync call completes.
  Assert.Throws<Exception>(async () => await SystemUnderTest.FailAsync());
}
// Does NOT pass.
[Test]
public void BadFailureTest_AssertThrows()
{
  Assert.Throws<Exception>(() => SystemUnderTest.FailAsync());
}

Così, l'attuale supporto per un pronto async ThrowsException/tiri non è grande. Nel mio test unità di codice, utilizzare un tipo molto simile al AssertEx in Figura 3. Questo tipo è piuttosto semplicistico, in quanto genera solo nudi oggetti eccezione invece di fare asserzioni, ma questo stesso codice funziona in tutte le principali unità di Framework di test.

Figura 3 la classe AssertEx per testare le eccezioni in modo asincrono

using System;
using System.Threading.Tasks;
public static class AssertEx
{
  public static async Task<TException> 
    ThrowsAsync<TException>(Func<Task> action,
    bool allowDerivedTypes = true) where TException : Exception
  {
    try
    {
      await action();
    }
    catch (Exception ex)
    {
      if (allowDerivedTypes && !(ex is TException))
        throw new Exception("Delegate threw exception of type " +
          ex.GetType().Name + ", but " + typeof(TException).Name +
          " or a derived type was expected.", ex);
      if (!allowDerivedTypes && ex.GetType() != typeof(TException))
        throw new Exception("Delegate threw exception of type " +
          ex.GetType().Name + ", but " + typeof(TException).Name +
          " was expected.", ex);
      return (TException)ex;
    }
    throw new Exception("Delegate did not throw expected exception " +
      typeof(TException).Name + ".");
  }
  public static Task<Exception> ThrowsAsync(Func<Task> action)
  {
    return ThrowsAsync<Exception>(action, true);
  }
}

In questo modo async task unit test utilizzare un più moderno ThrowsAsync invece di ExpectedExceptionAttribute, come questo:

[TestMethod]
public async Task FailureTest_AssertEx()
{
  var ex = await AssertEx.ThrowsAsync(() 
    => SystemUnderTest.FailAsync());
}

Simulazioni e Async Stubs

A mio parere, solo il codice più semplice può essere testato senza un qualche tipo di stub, finto, falso o altro tale dispositivo. In questo articolo introduttivo, limiterò a fare riferimento a tutti questi test assistenti come simulazioni. Quando si utilizza le simulazioni, è utile programma di interfacce anziché implementazioni. Metodi asincroni funzionano perfettamente anche con interfacce; il codice in Figura 4 dimostra come il codice può consumare un'interfaccia con un metodo asincrono.

Figura 4 utilizzando un metodo asincrono da un'interfaccia

public interface IMyService
{
  Task<int> GetAsync();
}
public sealed class SystemUnderTest
{
  private readonly IMyService _service;
  public SystemUnderTest(IMyService service)
  {
    _service = service;
  }
  public async Task<int> RetrieveValueAsync()
  {
    return 42 + await _service.GetAsync();
  }
}

Con questo codice, è abbastanza facile creare un'implementazione di test dell'interfaccia e passarlo al sistema sotto test. Figura 5 illustrato come testare i tre casi principali stub: successo asincrona, fallimento asincrono e sincroni. Fallimento e successo asincrono sono i primari due scenari per il test di codice asincrono, ma è anche importante testare il caso sincrono. Questo è perché l'operatore di attesa si comporta diversamente se la sua awaitable è già stata completata. Il codice in Figura 5 utilizza il Moq beffardo quadro per generare le implementazioni di stub.

Figura 5 Stub implementazioni per codice asincrono

[TestMethod]
public async Task RetrieveValue_SynchronousSuccess_Adds42()
{
  var service = new Mock<IMyService>();
  service.Setup(x => x.GetAsync()).Returns(() => Task.FromResult(5));
  // Or: service.Setup(x => x.GetAsync()).ReturnsAsync(5);
  var system = new SystemUnderTest(service.Object);
  var result = await system.RetrieveValueAsync();
  Assert.AreEqual(47, result);
}
[TestMethod]
public async Task RetrieveValue_AsynchronousSuccess_Adds42()
{
  var service = new Mock<IMyService>();
  service.Setup(x => x.GetAsync()).Returns(async () =>
  {
    await Task.Yield();
    return 5;
  });
  var system = new SystemUnderTest(service.Object);
  var result = await system.RetrieveValueAsync();
  Assert.AreEqual(47, result);
}
[TestMethod]
public async Task RetrieveValue_AsynchronousFailure_Throws()
{
  var service = new Mock<IMyService>();
  service.Setup(x => x.GetAsync()).Returns(async () =>
  {
    await Task.Yield();
    throw new Exception();
  });
  var system = new SystemUnderTest(service.Object);
  await AssertEx.ThrowsAsync(system.RetrieveValueAsync);
}

Parlando di beffardo quadri, c'è un po ' di sostegno che possono dare all'asincrono unit test, pure. Considerare per un momento quello che dovrebbe essere il comportamento predefinito di un metodo, se è stato specificato nessun comportamento. Alcuni quadri beffardo (come Microsoft Stubs) predefinito per la generazione di un'eccezione; altri (come Moq) restituirà un valore predefinito. Quando un metodo asincrono restituisce un'attività <T>, un comportamento predefinito ingenuo sarebbe tornare predefinito (Task <T>), in altre parole, un compito null, che causerà un'eccezione NullReferenceException.

Questo comportamento è indesiderabile. Sarebbe stato un comportamento predefinito più ragionevole per i metodi asincroni per restituire il metodo Task. FromResult­(default (t)) — che è, un compito che è completato con il valore predefinito di T. In questo modo il sistema sotto test per utilizzare l'attività restituita. MOQ implementato questo stile di comportamento predefinito per i metodi asincroni in versione 4.2 di Moq. A mia conoscenza, al momento, è l'unica libreria beffarda che utilizza impostazioni predefinite async-friendly come quello.

Conclusioni

Async e attendono sono stati circa dall'introduzione del Visual Studio 2012, abbastanza a lungo per alcune procedure consigliate emergere. Il framework di test di unità e componenti di supporto come beffardo librerie sono convergenti verso coerenza async-friendly supporto. Asincrono unit test oggi è già una realtà, e otterrà ancora meglio in futuro. Se non l'hai fatto così di recente, ora è un buon momento per aggiornare il framework di test di unità e beffardo librerie per garantirvi il migliore supporto async.

Il framework di test di unità stanno convergendo dalla async void unit test e verso attività async unit test. Se avete qualsiasi async void unit test, vi consiglio di che cambiarli oggi a async task unit test.

Prevedo che nei prossimi anni coppia vedrai molto migliore supporto per la verifica di casi di fallimento in async unit test. Fino a quando il framework di unit test ha un buon supporto, suggerisco si utilizza il tipo AssertEx citato in questo articolo, o qualcosa di simile che è più su misura per il framework di test di unità particolare.

Corretto asincrono unit test è una parte importante della storia async e sono entusiasta di vedere questi quadri e librerie adottano async. Uno dei miei primi colloqui lampo era circa async unit test alcuni anni fa quando async era ancora in anteprima tecnologica di comunità, ed è molto più facile da fare in questi giorni!


Stephen Cleary è un programmatore che vivono nel nord del Michigan, padre e marito. Ha lavorato con multithreading e asincrona di programmazione per 16 anni e ha utilizzato il supporto async in Microsoft .NET Framework poiché la prima anteprima di tecnologia di comunità. Egli è l'autore di "Concorrenza in c# Cookbook" (o ' Reilly Media, 2014).  Sua homepage, compreso il suo blog, è a stephencleary.com.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: James McCaffrey