2013-03-24 15 views
9

Kiedy analizuję pokrycie kodu w Visual Studio 2012, każda z oczekujących linii w metodach asynchronicznych jest wyświetlana jako nieobjęta, nawet jeśli są one oczywiście wykonywane, ponieważ moje testy mijają. Raport pokrycia kodu mówi, że nieprzykrytą metodą jest MoveNext, której nie ma w moim kodzie (może to generowane przez kompilator).Pokrycie kodu dla metod asynchronicznych

Czy istnieje sposób, aby ustalić raportowanie zasięgu kodu dla metod asynchronicznych?

Uwaga:

ja po prostu zabrakło pokrycia za pomocą NCover, a numery pokrycia zrobić dużo więcej sensu przy użyciu tego narzędzia. Na razie obejdę ten problem.

Odpowiedz

4

Może się to zdarzyć najczęściej, jeśli operacja, na którą czekasz, zostanie ukończona, zanim będzie oczekiwana.

Polecam testowanie co najmniej synchronicznych i asynchronicznych sytuacji związanych z powodzeniem, ale dobrym pomysłem jest również testowanie błędów synchronicznych i asynchronicznych oraz anulowanie.

+1

Metody są wszystkie akcesoria i testy przechodzą. Wygląda na to, że napotykam na ograniczenie narzędzia. – Jacob

+0

Dobrze, ale czy operacje są już zakończone w momencie "oczekiwania"? –

+0

Gotcha ... więc naprawdę musisz przetestować te scenariusze dla każdego wystąpienia oczekiwania? Jeśli masz metodę z 5 oczekiwaniami, musisz napisać co najmniej 15 przypadków testowych, aby uzyskać 100% pokrycia? To wydaje mi się błędem. To wydaje mi się bardziej podobne do testowania mechanizmów asynchronicznych emitowanych przez kompilator niż testowanie własnego kodu. – Jacob

-1

Utworzono test biegacza, który uruchamia blok kodu wiele razy i zmienia zadanie, które jest opóźnione przy użyciu fabryki. Jest to świetne rozwiązanie do testowania różnych ścieżek za pomocą prostych bloków kodu. W przypadku bardziej złożonych ścieżek możesz utworzyć test na ścieżkę.

[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

Są sytuacje, w których nie obchodzi mnie testowanie asynchronicznej natury metody, ale po prostu chcę pozbyć się częściowego pokrycia kodu. Używam poniższej metody rozszerzania, aby tego uniknąć i działa to dobrze dla mnie.

Ostrzeżenie "Thread.Sleep" użyty tutaj!

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

i wykorzystanie jest podobna do ReturnsAsync Setup MOQ za.

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

Przyczyna, dla której kod nie jest pokazany jako objęty, ma związek ze sposobem implementacji metod asynchronicznych. Kompilator C# faktycznie tłumaczy kod w metodach asynchronicznych na klasę, która implementuje automat stanów i przekształca oryginalną metodę w kod pośredniczący, który zainicjował i wywołał ten automat stanów. Ponieważ ten kod jest generowany w zespole, jest uwzględniany w analizie pokrycia kodu.

Jeśli używasz zadania, które nie jest kompletne w momencie, gdy wykonywany jest kod, maszyna wygenerowana przez kompilator pobiera wywołanie zwrotne zakończenia, aby wznowić po zakończeniu zadania. To w pełni wykorzystuje kod maszynowy stanu i powoduje całkowite pokrycie kodu (przynajmniej dla narzędzi pokrycia kodu na poziomie instrukcji).

Typowy sposób na uzyskanie zadania, które nie jest kompletne w danym momencie, ale zakończy się w pewnym momencie, to użycie Task.Delay w teście jednostki. Jednak jest to zazwyczaj zła opcja, ponieważ opóźnienie czasowe jest albo zbyt małe (i powoduje nieprzewidywalne pokrycie kodu, ponieważ czasami zadanie jest zakończone przed uruchomieniem kodu) lub zbyt duże (niepotrzebnie spowalniające testy w dół).

Lepszym rozwiązaniem jest użycie opcji "czekaj na Task.Yield()". To nastąpi natychmiast, ale wywołaj kontynuację, gdy tylko zostanie ustawiona.

Inną opcją - choć nieco absurdalną - jest wdrożenie własnego, godnego zaufania wzorca, który ma semantykę raportowania niekompletną, dopóki połączenie zwrotne nie zostanie nawiązane, a następnie natychmiast zakończone. To zasadniczo zmusza komputer stanu do ścieżki asynchronicznej, zapewniając pełne pokrycie.

Oczywiście nie jest to idealne rozwiązanie. Najbardziej niefortunnym aspektem jest to, że wymaga modyfikacji kodu produkcyjnego w celu ograniczenia ograniczenia narzędzia. Wolałbym, aby narzędzie zasięgu kodu ignorowało części maszyny asynchronicznej generowane przez kompilator. Ale do tego czasu nie ma zbyt wielu opcji, jeśli naprawdę chcesz spróbować uzyskać pełny zasięg kodu.

Pełniejsze wyjaśnienie tego hack, można znaleźć tutaj: http://blogs.msdn.com/b/dwayneneed/archive/2014/11/17/code-coverage-with-async-await.aspx

Powiązane problemy