由于计时和线程问题,NUnit 测试执行失败

NUnit test execution fails because of timing and threading problems

我们在 .NET 应用程序中进行了大量测试。其中一些测试与时间相关,一些代码与 .NET TPL 是多线程的。几个星期以来,我们遇到了很多问题。之前,测试 运行ning 每次都成功。

我用一个简单的计时器来举例:

在定时器启动的class NotSoParallelOps Method ScheduleExecution (L107) a normal System.Threading.Timer will be started. The corresponding tests are in NotSoParallelOpsTests方法ScheduleExecutionWithStop (L79)中,测试线程会被阻塞一段时间,然后在定时器超时时进行评估。

TPL 的另一个示例:

public class Foo
{
    public void Start(int delay)
    {
        Task.Run(async delegate
        {
            await Task.Delay(delay);
            TaskDone?.Invoke(this, EventArgs.Empty);
        });
    }

    public event EventHandler TaskDone;
}

[TestFixture]
public class FooTests
{
    [Test]
    public void TestRunner()
    {
        // Arrange
        var foo = new Foo();
        var resetEvent = new ManualResetEvent(false);

        foo.TaskDone += delegate
        {
            resetEvent.Set();
        };

        // Act
        foo.Start(1000);
        var result = resetEvent.WaitOne(2000);

        // Assert
        Assert.IsTrue(result, "Task not done in time");
    }
}

新任务将从 Task.Run 开始。由于某些原因,任务被延迟,然后引发了一个事件。测试注册到事件并检查任务是否及时执行。

如果测试是使用 Visual Studio Resharper UnitTest Explorer 执行的,它使用的是 NUnit 测试 运行ner,测试大多是成功的。如果将使用 nunit-console,则测试为 failing very often but also succeed sometimes。不仅在本地机器上,在 GitHub Workflow 运行 用户和 GitLab CI 运行 用户上也是如此。同样的问题,同样的结果。

一个想法是更改等待计时器测试的时间。它有效,但为什么现在要这样做?几个 month/years 的测试是成功的。此外,构建基础架构变得更快、性能更高。

我将同样的想法应用到 Task.Run 测试中。但它不会 运行 即使我将时间设置为 60 秒。最让人困惑的是,测试在我的本地机器上运行良好,但在 GitHub Workflow 运行ners 和 GitLab CI 运行ners.

有谁知道我可以做些什么来解决这个问题,或者有更好的办法来测试这些代码行吗?

您可以尝试重写您的测试以使用 TaskCompletionSourceCancellationToken

如果您不需要 EventArgs,那么测试的重写版本将如下所示:

[Test]
public async Task TestRunner()
{
    // Arrange
    var foo = new Foo();
    var fooTaskCompleted = new TaskCompletionSource<object>();
    var timeoutPolicy = new CancellationTokenSource(2000);
    timeoutPolicy.Token.Register((future) => ((TaskCompletionSource<object>)future).TrySetCanceled(), useSynchronizationContext: false, state: fooTaskCompleted);

    foo.TaskDone += delegate
    {
        fooTaskCompleted.SetResult(null);
    };

    // Act
    foo.Start(1000);
    await fooTaskCompleted.Task;

    // Assert
    Assert.IsFalse(fooTaskCompleted.Task.IsCanceled);
    Assert.IsFalse(fooTaskCompleted.Task.IsFaulted);
}

如果你需要 EventArgs 那么你可以像这样重写 TestRunner 方法:

[Test]
public async Task TestRunner()
{
    // Arrange
    var foo = new Foo();
    var fooTaskCompleted = new TaskCompletionSource<EventArgs>();
    var timeoutPolicy = new CancellationTokenSource(2000);
    timeoutPolicy.Token.Register((future) => ((TaskCompletionSource<EventArgs>)future).TrySetCanceled(), useSynchronizationContext: false, state: fooTaskCompleted);

    foo.TaskDone += (s, e) =>
    {
        fooTaskCompleted.TrySetResult(e);
    };

    // Act
    foo.Start(1000);
    var eventArgs = await fooTaskCompleted.Task;

    // Assert
    Assert.IsFalse(fooTaskCompleted.Task.IsCanceled);
    Assert.IsFalse(fooTaskCompleted.Task.IsFaulted);
}