如何对异步调用的进度报告进行单元测试?

How to unit-test the progress report of an async call?

目标

我想测试一些接受 IProgress 的异步调用的进度报告。 我要验证:

我想使用尽可能少的框架 -> 不使用另一个 3.party 库的解决方案会很好。

Problem/My 尝试

我试着写了一个简单的异步任务测试方法来实现这个。我调用方法来测试 await 例如。 await asyncCallToTest(new Progress((p) => { Assert.someting(p); }))。也许在 lambda 中使用堆栈变量。完整示例稍后。

然而,这导致测试成功,即使它不应该是可能的,或者有时测试通过,然后在没有更改的情况下失败....

代码

我通过简单的独立测试重现了这些问题 class:

using System;
using System.Threading.Tasks;

using NUnit.Framework;

namespace FastProgressReporter
{
    public class Tests
    {
        [Test]
        public async Task SampleOkTest()
        {
            await TestReporter(new Progress<double>((p) => {
                Assert.GreaterOrEqual(p, 0);
                Assert.LessOrEqual(p, 1);
            }));
            //Sometimes fails, sometims passes. Why?
        }

        [Test]
        public async Task SampleFailTest()
        {
            await TestReporter(new Progress<double>((p) => {
                Assert.GreaterOrEqual(p, 2); // Has to Fail, however passes.
                Assert.LessOrEqual(p, 3);
            }));
        }

        [Test]
        public async Task ReportsIncremental()
        {
            double latestProgress = 0;
            await TestReporter(new Progress<double>((p) => {
                p = 1 - p; //Force Decrement...
                Assert.GreaterOrEqual(latestProgress, p); //Should fail on second call
                latestProgress = p;
            }));
        }

        [Test]
        public async Task ReportsOneOneOrMoreTimes()
        {
            double maxProgress = 0;
            await TestReporter(new Progress<double>((p) => {
                maxProgress = p > maxProgress ? p : maxProgress;
            }));
            Assert.AreEqual(0, maxProgress); //Should always fail
            //Sometimes fails, sometims passes. Why?
        }

        // Methode under test
        public static async Task TestReporter(IProgress<double> progress)
        {
            progress.Report(0);
            await Task.Run(() => { /*Do nothing, however await*/ });
            progress.Report(0.5);
            await Task.Run(() => { /*Do nothing, however await*/ });
            progress.Report(1);
        }
    }
}

环境

VisualStudio 2019,.net-framework 4.7.2,NUnit3 测试项目

问题

我如何可靠、正确地实现我的目标?

相关问题

这使用了我试图避免的 3 方库,并且直接在测试方法内部调用 .Report(),而不是在测试调用的异步方法中调用

感谢您的帮助。

您可以将所有进度报告添加到列表中,然后对集合进行断言。项目的数量将等于进度报告的数量并且顺序相同。您可以检查与进度报告相关的任意数量的条件。

关于你的测试失败,这很奇怪,因为如果它们 运行 正确,那么每次你 运行 它们应该有相同的结果,因为不依赖于状态。唯一的依赖是测试运行器实际上 运行 异步测试。据我所知,NUnit 应该能够做到这一点,它支持异步测试。

Progress 实例捕获当前的 SynchronizationContext 并使用它运行回调。这对 UI 特别有用,因为进度回调将在 UI 线程上执行,因此您无需担心。 NUnit 很可能有自己的自定义 SynchronizationContext 用于执行异步测试,您的测试有时会失败,有时不会,这可能与相关测试实际完成后正在执行的进度报告有关。在这种情况下,您实际上可能会在一个不同的测试中得到一个测试的断言异常。

如果这是真的,那么您可能需要自己实现不使用 SynchronizationContext 的 IProgress 才能正确执行进度报告。基本上,您想要的是在调用报告方法时执行回调。您的实现可以做到这一点,无论何时调用进度方法,它都会将报告的值添加到列表中,该列表通过 属性 公开,然后使用 属性.[=10 执行断言=]

给你 st运行ge 结果的很可能是 Progress class 的使用,因为它在内部依赖于 SynchronizationContext 来执行回调。

解决方案

按照@Andrei15193 的建议,我尝试自己实现 IProgress 以摆脱 Progress class。

private class UnitTestProgress<T> : IProgress<T>
{
    private Action<T> handler;

    public UnitTestProgress(Action<T> handler)
    {
        this.hander = handler?? throw new ArgumentNullException(nameof(hander));
    }

    public void Report(T value)
    {
        handler(value);
    }
}

我在这里选择了一个非常简单的class作为实现。

据我测试,如果 Progress 被我的新 class 替换,问题就解决了,单元测试是可靠的。