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

问题描述

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

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

在类NotSoParallelOps方法ScheduleExecution(L107)中,将启动常规System.Threading.Timer。相应的测试在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开始。由于某些原因,任务被延迟,然后引发事件。测试将注册到事件并检查任务是否及时执行。

如果使用正在使用NUnit测试运行程序的Visual Studio Resharper UnitTest Explorer执行测试,则测试大部分会成功。如果将使用nunit-console,则测试为failing very often,但也为succeed sometimes。不仅在本地计算机上,在GitHub Workflow运行程序和GitLab CI运行程序上也是如此。同样的问题,同样的结果。

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

我对Task.Run测试应用了相同的想法。但是,即使我将时间设置为60秒,它也不会运行。最令人困惑的是,该测试在我的本地计算机上运行正常,但在GitHub Workflow运行程序和GitLab CI运行程序上却无法正常运行。

有人知道我该怎么做才能解决此问题,或者有更好的主意来测试这种代码行吗?

  • NUnit控制台:3.11.1
  • NUnit TestAdapter:3.17.0

解决方法

您可以尝试使用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(),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);
}

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...