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