2013-03-24 73 views
9

當我分析Visual Studio 2012中的代碼覆蓋率時,async方法中的任何await行都顯示爲未被覆蓋,即使它們在我的測試通過後顯然執行。代碼覆蓋率報告說,未覆蓋的方法是MoveNext,這是不存在在我的代碼(也許這是編譯器生成的)。異步方法的代碼覆蓋率

有沒有辦法解決了異步方法的代碼覆蓋率報告?

注意

我只是跑覆蓋使用NCover,並覆蓋人數,使用此工具更有意義。作爲現在的解決方法,我將切換到此。

回答

4

這可如果它等待完成之前你在等待手術最常見的發生。

我建議您測試至少同步和異步成功的情況,但測試同步和異步錯誤和取消也是一個好主意。

+1

的方法都完成,並測試合格。它看起來像我遇到了工具的限制。 – Jacob 2013-03-24 22:17:00

+0

沒錯,但是在await這個位置已經完成了操作嗎? – 2013-03-24 22:52:12

+0

難題...所以你真的不得不爲每個await實例測試這些場景嗎?如果你有一個有5個等待的方法,你必須寫至少15個測試用例才能獲得100%的覆蓋率?這對我來說似乎是一個錯誤。這對我來說更像是測試編譯器發出的異步機制,而不是測試自己的代碼。 – Jacob 2013-03-24 23:25:05

-1

我創建了一個測試運行器,它多次運行一段代碼並改變使用工廠延遲的任務。這非常適合通過簡單的代碼塊測試不同的路徑。對於更復雜的路徑,您可能需要爲每個路徑創建一個測試。

[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

還有,我不關心測試方法的異步性質,但只是想擺脫的部分代碼覆蓋率的情況。我使用下面的擴展方法來避免這種情況,它對我來說工作得很好。

警告「Thread.Sleep」在這裏使用!

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

和使用類似於起訂量的ReturnsAsync設置。

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

代碼未顯示爲被覆蓋的原因與如何實現異步方法有關。 C#編譯器實際上在轉換異步方法的代碼轉換成實現一個狀態機一類,並且將原始方法成初始化並調用該狀態機存根。由於此代碼是在您的程序集中生成的,因此它包含在代碼覆蓋率分析中。

如果您在執行覆蓋的代碼時使用的任務未完成,則編譯器生成的狀態機會掛起一個完成回調,以在任務完成時恢復。這更完全地執行狀態機代碼,並導致完整的代碼覆蓋(至少對於語句級代碼覆蓋工具)。

獲得目前尚未完成的任務的常用方法是在單元測試中使用Task.Delay。然而,這通常是一個糟糕的選擇,因爲時間延遲太小(並且導致不可預知的代碼覆蓋率,因爲有時任務在代碼運行之前完成)或太大(不必要地減慢了測試的速度)。

一個更好的選擇是使用 「等待Task.Yield()」。這將立即返回,但一旦設置就立即調用延續。

另一種選擇 - 儘管有點荒謬 - 是實現你自己的等待模式,它具有報告不完整的語義,直到連續回調被連接,然後立即完成。這基本上強制狀態機進入異步路徑,提供完整的覆蓋。

可以肯定,這不是一個完美的解決方案。最不幸的是它需要修改生產代碼來解決工具的侷限性。我更喜歡代碼覆蓋工具忽略編譯器生成的異步狀態機的部分。但是在這種情況發生之前,如果你真的想嘗試獲得完整的代碼覆蓋率,那麼沒有太多的選擇。

一個這個技巧的更完整的解釋可以在這裏找到:http://blogs.msdn.com/b/dwayneneed/archive/2014/11/17/code-coverage-with-async-await.aspx