当我在Visual Studio 2012中分析代码覆盖率时,异步方法中的任何等待行都显示为未覆盖,即使它们显然正在执行,因为我的测试正在通过。代码覆盖率报告说,未覆盖的方法是MoveNext
,这在我的代码中不存在(可能是编译器生成的)。
有没有办法修复异步方法的代码覆盖率报告?
注意:
我刚刚使用NCover运行覆盖,使用该工具覆盖数量更有意义。作为现在的解决方法,我将改用它。
答案 0 :(得分:5)
如果您正在等待的操作在等待之前完成,这种情况最常发生。
我建议您至少测试同步和异步成功情况,但测试同步和异步错误和取消也是一个好主意。
答案 1 :(得分:2)
代码未显示为覆盖的原因与异步方法的实现方式有关。 C#编译器实际上将异步方法中的代码转换为实现状态机的类,并将原始方法转换为初始化并调用该状态机的存根。由于此代码是在程序集中生成的,因此它包含在代码覆盖率分析中。
如果在正在执行的代码执行时使用未完成的任务,则编译器生成的状态机将挂起完成回调以在任务完成时恢复。这样可以更完整地运行状态机代码,从而实现完整的代码覆盖(至少对于语句级代码覆盖工具而言)。
获取当前未完成但在某个时刻完成的任务的常用方法是在单元测试中使用Task.Delay。但是,这通常是一个糟糕的选择,因为时间延迟太小(并导致不可预测的代码覆盖率,因为有时任务在测试运行的代码之前完成)或太大(不必要地减慢测试速度)。
更好的选择是使用"等待Task.Yield()"。这将立即返回,但一旦设置就会立即调用。
另一种选择 - 尽管有些荒谬 - 是实现你自己的等待模式,它具有报告的语义不完整,直到连接回调被连接,然后立即完成。这基本上迫使状态机进入异步路径,提供完整的覆盖。
可以肯定的是,这不是一个完美的解决方案。最不幸的是,它需要修改生产代码以解决工具的限制。我更希望代码覆盖工具忽略编译器生成的异步状态机部分。但是在这种情况发生之前,如果您真的想要尝试获得完整的代码覆盖率,则没有太多选择。
可以在此处找到有关此黑客攻击的更完整说明:http://blogs.msdn.com/b/dwayneneed/archive/2014/11/17/code-coverage-with-async-await.aspx
答案 2 :(得分:1)
在某些情况下,我并不关心测试方法的异步性质,只是想摆脱部分代码覆盖。我使用下面的扩展方法来避免这种情况,它对我来说效果很好。
警告" 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);
}
,其用法类似于Moq的 ReturnsAsync 设置。
_sampleMock.Setup(s => s.SampleMethodAsync()).ReturnsAsyncDelayed(response);
答案 3 :(得分:-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;
}
}