异步方法的代码覆盖率

10
当我在Visual Studio 2012中分析代码覆盖率时,任何异步方法中的await行都显示为未覆盖,尽管它们显然正在执行(因为我的测试通过了)。代码覆盖报告显示未覆盖的方法是MoveNext,这不在我的代码中(可能是由编译器生成的)。
有没有办法修复异步方法的代码覆盖报告?
注意: 我刚刚使用NCover运行了覆盖率,并且使用该工具的覆盖率数字更加合理。暂时作为解决方法,我将切换到该工具。
4个回答

5

如果您等待的操作在等待之前已经完成,最常见的情况是会发生这种情况。

我建议您测试至少同步和异步成功的情况,但测试同步和异步错误和取消也是一个好主意。


1
所有的方法都已经完成,测试也都通过了。看起来我遇到了工具的限制。 - Jacob
await点时,操作已经完成了吗? - Stephen Cleary
明白了...所以你真的必须为每个await实例测试这些场景吗?如果你有一个带有5个awaits的方法,你至少需要编写15个测试用例才能获得100%的覆盖率?对我来说,这似乎是一个错误。这更像是测试编译器发出的异步机制,而不是测试你自己的代码。 - Jacob
我同意你不应该测试异步机制,但是在await中包含了几个不同的执行路径。因此,请考虑您方法的所有语义:如果等待的任务没有完成,它应该完成吗?如果等待的任务已经完成,它应该同步完成吗?它应该传播异常吗?一旦您开始涵盖所有实际语义,您可能会发现代码覆盖率不是问题。在我的AsyncEx库中,有一些地方是无法进行代码覆盖的,所以我永远不会达到100%。这并不会让我夜不能寐。 :) - Stephen Cleary
1
我在这个问题上写了一篇博客文章。请查看 http://bernhard-richter.blogspot.no/2014/09/asyncawait-and-code-coverage.html - seesharper

2
代码没有被覆盖的原因与异步方法的实现方式有关。C#编译器实际上将异步方法中的代码转换为实现状态机的类,并将原始方法转换为初始化和调用该状态机的存根。由于此代码在您的程序集中生成,因此它包含在代码覆盖分析中。
如果您在执行代码时使用了一个尚未完成的任务,则编译器生成的状态机会挂接完成回调以在任务完成时恢复。这更完全地运用了状态机代码,并导致完整的代码覆盖率(至少对于语句级别的代码覆盖工具)。
获得一个在某个时间点上尚未完成但最终将完成的任务的常见方法是在单元测试中使用Task.Delay。然而,这通常是一个不好的选择,因为时间延迟要么太短(导致代码覆盖率不可预测,因为有时任务在进行测试运行之前就已经完成),要么太长(使测试变慢)。
更好的选择是使用“await Task.Yield()”。这将立即返回,但一旦设置就会调用继续操作。
另一个选择 - 虽然有点荒谬 - 是实现自己的可等待模式,它具有报告不完整的语义,直到连接回调被挂起,然后立即完成。这基本上强制状态机进入异步路径,提供完整的覆盖范围。
当然,这不是一个完美的解决方案。最不幸的方面是,它需要修改生产代码来解决工具的限制。我更希望代码覆盖工具忽略编译器生成的异步状态机的部分。但在此之前,如果您真的想尝试获得完整的代码覆盖率,就没有太多选择。
关于这个技巧的更完整的解释可以在这里找到:http://blogs.msdn.com/b/dwayneneed/archive/2014/11/17/code-coverage-with-async-await.aspx

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接