2013-03-24 133 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