2013-03-24 14 views
9

Quando analizzo la copertura del codice in Visual Studio 2012, tutte le linee di attesa nei metodi asincroni vengono visualizzate come non coperte anche se sono ovviamente eseguite da quando i miei test sono passati. Il report sulla copertura del codice dice che il metodo scoperto è MoveNext, che non è presente nel mio codice (forse è generato dal compilatore).Copertura del codice per i metodi asincroni

C'è un modo per correggere i report di copertura del codice per i metodi asincroni?

Nota:

Ho appena eseguito la copertura utilizzando NCover, ei numeri di copertura ha molto più senso utilizzando tale strumento. Come soluzione per il momento, passerò a quello.

risposta

4

Ciò può accadere più comunemente se l'operazione in attesa è completata prima che sia attesa.

Ti consiglio di testare almeno le situazioni di successo sincrone e asincrone, ma è anche una buona idea verificare errori e cancellazioni sincrone e asincrone.

+1

I metodi sono tutti completando, e le prove stanno passando. Sembra che sto incontrando una limitazione dello strumento. – Jacob

+0

Giusto, ma le operazioni sono già state completate al momento dell '"attesa"? –

+0

Gotcha ... quindi dovresti davvero testare quegli scenari per ogni istanza di attesa? Se avessi un metodo con 5 attese, dovresti scrivere almeno 15 casi di test per ottenere una copertura del 100%? Sembra un insetto per me. Mi sembra più come testare i meccanismi asincroni emessi dal compilatore che testare il tuo codice. – Jacob

-1

Ho creato un test runner che esegue un blocco di codice più volte e varia l'attività che viene ritardata utilizzando una fabbrica. Questo è ottimo per testare i diversi percorsi attraverso semplici blocchi di codice. Per i percorsi più complessi potresti voler creare un test per percorso.

[TestMethod] 
public async Task ShouldTestAsync() 
{ 
    await AsyncTestRunner.RunTest(async taskFactory => 
    { 
     this.apiRestClient.GetAsync<List<Item1>>(NullString).ReturnsForAnyArgs(taskFactory.Result(new List<Item1>())); 
     this.apiRestClient.GetAsync<List<Item2>>(NullString).ReturnsForAnyArgs(taskFactory.Result(new List<Item2>())); 

     var items = await this.apiController.GetAsync(); 

     this.apiRestClient.Received().GetAsync<List<Item1>>(Url1).IgnoreAwait(); 
     this.apiRestClient.Received().GetAsync<List<Item2>>(Url2).IgnoreAwait(); 

     Assert.AreEqual(0, items.Count(), "Zero items should be returned."); 
    }); 
} 

public static class AsyncTestRunner 
{ 
    public static async Task RunTest(Func<ITestTaskFactory, Task> test) 
    { 
     var testTaskFactory = new TestTaskFactory(); 
     while (testTaskFactory.NextTestRun()) 
     { 
      await test(testTaskFactory); 
     } 
    } 
} 

public class TestTaskFactory : ITestTaskFactory 
{ 
    public TestTaskFactory() 
    { 
     this.firstRun = true; 
     this.totalTasks = 0; 
     this.currentTestRun = -1; // Start at -1 so it will go to 0 for first run. 
     this.currentTaskNumber = 0; 
    } 

    public bool NextTestRun() 
    { 
     // Use final task number as total tasks. 
     this.totalTasks = this.currentTaskNumber; 

     // Always return has next as turn for for first run, and when we have not yet delayed all tasks. 
     // We need one more test run that tasks for if they all run sync. 
     var hasNext = this.firstRun || this.currentTestRun <= this.totalTasks; 

     // Go to next run so we know what task should be delayed, 
     // and then reset the current task number so we start over. 
     this.currentTestRun++; 
     this.currentTaskNumber = 0; 
     this.firstRun = false; 

     return hasNext; 
    } 

    public async Task<T> Result<T>(T value, int delayInMilliseconds = DefaultDelay) 
    { 
     if (this.TaskShouldBeDelayed()) 
     { 
      await Task.Delay(delayInMilliseconds); 
     } 

     return value; 
    } 

    private bool TaskShouldBeDelayed() 
    { 
     var result = this.currentTaskNumber == this.currentTestRun - 1; 
     this.currentTaskNumber++; 
     return result; 
    } 

    public async Task VoidResult(int delayInMilliseconds = DefaultDelay) 
    { 
     // If the task number we are on matches the test run, 
     // make it delayed so we can cycle through them. 
     // Otherwise this task will be complete when it is reached. 
     if (this.TaskShouldBeDelayed()) 
     { 
      await Task.Delay(delayInMilliseconds); 
     } 
    } 

    public async Task<T> FromResult<T>(T value, int delayInMilliseconds = DefaultDelay) 
    { 
     if (this.TaskShouldBeDelayed()) 
     { 
      await Task.Delay(delayInMilliseconds); 
     } 

     return value; 
    } 
} 
2

ci sono situazioni in cui non mi interessa circa verificare la natura asincrona di un metodo, ma voglio solo per sbarazzarsi della copertura del codice parziale. Io uso il metodo di estensione di seguito per evitare questo e funziona bene per me.

Avviso "Thread.Sleep" usato qui!

public static IReturnsResult<TClass> ReturnsAsyncDelayed<TClass, TResponse>(this ISetup<TClass, Task<TResponse>> setup, TResponse value) where TClass : class 
{ 
    var completionSource = new TaskCompletionSource<TResponse>(); 
    Task.Run(() => { Thread.Sleep(200); completionSource.SetResult(value); }); 
    return setup.Returns(completionSource.Task); 
} 

e l'utilizzo è simile a ReturnsAsync Setup del Moq.

_sampleMock.Setup(s => s.SampleMethodAsync()).ReturnsAsyncDelayed(response); 
1

Il motivo per cui il codice non viene mostrato come coperto riguarda il modo in cui i metodi asincroni vengono implementati. Il compilatore C# traduce il codice in metodi asincroni in una classe che implementa una macchina a stati e trasforma il metodo originale in uno stub inizializzato e invoca quella macchina a stati. Poiché questo codice viene generato nell'assieme, è incluso nell'analisi della copertura del codice.

Se si utilizza un'attività che non è completa al momento dell'esecuzione del codice, la macchina di stato generata dal compilatore aggancia un callback di completamento per riprendere al termine dell'attività. Questo esercita più completamente il codice macchina dello stato e fornisce una copertura completa del codice (almeno per gli strumenti di copertura del codice a livello di istruzione).

Un modo comune per ottenere un'attività che non è completa al momento, ma che a un certo punto verrà completata è l'uso di Task.Delay nel test dell'unità. Tuttavia, questa è generalmente un'opzione scadente perché il ritardo temporale è troppo piccolo (e comporta una copertura del codice imprevedibile perché a volte l'attività è completa prima dell'esecuzione del codice in fase di test) o troppo grande (rallentando inutilmente i test).

Un'opzione migliore è utilizzare "attendere Task.Yield()". Questo ritornerà immediatamente ma invocherà la continuazione non appena viene impostato.

Un'altra opzione, anche se un po 'assurda, consiste nell'implementare il proprio modello attendibile che ha la semantica della segnalazione incompleta finché non viene collegato un callback di continuazione e quindi viene immediatamente completato. Questo in pratica forza la macchina a stati nel percorso asincrono, fornendo la copertura completa.

Per essere sicuri, questa non è una soluzione perfetta. L'aspetto più sfortunato è che richiede la modifica del codice di produzione per affrontare una limitazione di uno strumento. Preferisco di gran lunga che lo strumento di copertura del codice ignori le parti della macchina a stati asincroni generate dal compilatore. Ma finché ciò non accade, non ci sono molte opzioni se si vuole veramente provare a ottenere una copertura completa del codice.

Una più completa spiegazione di questo hack può essere trovato qui: http://blogs.msdn.com/b/dwayneneed/archive/2014/11/17/code-coverage-with-async-await.aspx