【问题标题】:Code coverage for async methods异步方法的代码覆盖率
【发布时间】:2013-03-24 20:47:52
【问题描述】:

当我在 Visual Studio 2012 中分析代码覆盖率时,异步方法中的任何等待行都显示为未覆盖,即使它们显然正在执行,因为我的测试通过了。代码覆盖率报告说未发现的方法是MoveNext,它在我的代码中不存在(可能是编译器生成的)。

有没有办法修复异步方法的代码覆盖率报告?

注意

我刚刚使用 NCover 运行了覆盖率,而使用该工具的覆盖率数字更有意义。作为目前的解决方法,我将改用它。

【问题讨论】:

    标签: visual-studio-2012 mstest code-coverage async-await c#-5.0


    【解决方案1】:

    如果您正在等待的操作在等待之前完成,则最常发生这种情况。

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

    【讨论】:

    • 方法全部完成,测试通过。看来我遇到了该工具的限制。
    • 是的,但是在await 处操作是否已经完成?
    • 知道了......所以你真的必须为每个 await 实例测试这些场景吗?如果您有一个有 5 个等待的方法,您必须编写至少 15 个测试用例才能获得 100% 的覆盖率?这对我来说似乎是一个错误。在我看来,这更像是测试编译器发出的异步机制,而不是测试你自己的代码。
    • 我同意您不必测试异步机制,但是await 中包含了几种不同的执行路径。因此,请考虑您方法的所有语义:如果等待的任务未完成,它是否应该完成?如果等待的任务已经完成,它是否应该同步完成?它应该传播异常吗?一旦你开始覆盖所有的实际语义,你可能会发现代码覆盖不是问题。在我的 AsyncEx 库中,有几个地方的代码覆盖率是不可能的,所以我永远不会 100%。它不会让我彻夜难眠。 :)
    • 我写了一篇关于这个问题的博文。看看bernhard-richter.blogspot.no/2014/09/…
    【解决方案2】:

    代码未显示为被覆盖的原因与异步方法的实现方式有关。 C# 编译器实际上将异步方法中的代码转换为实现状态机的类,并将原始方法转换为初始化并调用该状态机的存根。由于此代码是在您的程序集中生成的,因此它包含在代码覆盖率分析中。

    如果您使用的任务在执行所覆盖的代码时尚未完成,编译器生成的状态机会挂接一个完成回调以在任务完成时恢复。这更完整地锻炼了状态机代码,并产生了完整的代码覆盖率(至少对于语句级代码覆盖率工具而言)。

    获取当前未完成但会在某个时间完成的任务的常用方法是在单元测试中使用 Task.Delay。然而,这通常是一个糟糕的选择,因为时间延迟要么太小(并导致不可预知的代码覆盖率,因为有时任务在被测试的代码运行之前完成)或太大(不必要地减慢测试速度)。

    更好的选择是使用“await Task.Yield()”。这将立即返回,但在设置后立即调用延续。

    另一种选择——虽然有点荒谬——是实现你自己的等待模式,它的语义是报告不完整,直到连接了一个延续回调,然后立即完成。这基本上强制状态机进入异步路径,提供完整的覆盖。

    可以肯定,这不是一个完美的解决方案。最不幸的方面是它需要修改生产代码以解决工具的限制。我更希望代码覆盖工具忽略编译器生成的异步状态机部分。但在此之前,如果您真的想尝试获得完整的代码覆盖率,则没有太多选择。

    可以在此处找到有关此 hack 的更完整说明:http://blogs.msdn.com/b/dwayneneed/archive/2014/11/17/code-coverage-with-async-await.aspx

    【讨论】:

      【解决方案3】:

      在某些情况下,我不关心测试方法的异步性质,而只是想摆脱部分代码覆盖。我使用下面的扩展方法来避免这种情况,它对我来说很好。

      此处使用了警告“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);
      

      【讨论】:

        【解决方案4】:

        我创建了一个测试运行程序,它多次运行一段代码并改变使用工厂延迟的任务。这对于通过简单的代码块测试不同的路径非常有用。对于更复杂的路径,您可能需要为每个路径创建一个测试。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-19
          • 2011-05-27
          • 1970-01-01
          • 2012-06-30
          • 1970-01-01
          • 2011-12-17
          相关资源
          最近更新 更多