首页 > 解决方案 > 由于计时和线程问题,NUnit 测试执行失败

问题描述

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

我举了一个简单的计时器示例:

在类NotSoParallelOps方法 ScheduleExecution (L107)System.Threading.Timer中将启动一个正常的。相应的测试在NotSoParallelOpsTests Method 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 测试运行程序执行测试,则测试大多是成功的。如果将使用 nunit-console,则测试经常失败,但有时也会成功。不仅在本地机器上也是如此,在 GitHub Workflow 运行器和 GitLab CI 运行器上也是如此。同样的问题,同样的结果。

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

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

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

标签: c#nunittask-parallel-librarygitlab-cigithub-actions

解决方案


您可以尝试重写您的测试以TaskCompletionSource使用CancellationToken.

如果您不需要,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);
}

推荐阅读