如何使用 TPL 和 TaskScheduler 编写单元测试

发布于 2024-12-11 06:37:26 字数 350 浏览 0 评论 0 原文

想象一下这样的函数:

private static ConcurrentList<object> list = new ConcurrentList<object>();
public void Add(object x)
{
   Task.Factory.StartNew(() =>
   {
      list.Add(x); 
   }
}

我不关心 fentry 何时添加到列表中,但我需要将它添加到最后(显然;))

我没有找到一种方法来正确地对这样的东西进行单元测试而不需要返回任何回调处理程序或某物。因此添加程序不需要的逻辑

你会怎么做?

Imagine a function like this:

private static ConcurrentList<object> list = new ConcurrentList<object>();
public void Add(object x)
{
   Task.Factory.StartNew(() =>
   {
      list.Add(x); 
   }
}

I don't care WHEN exactly the fentry is added to the list, but i need it to be added in the end ( obviously ;) )

I don't see a way to properly unittest stuff like this without returning any callback-handler or sth. and therefor adding logic that's not required for the program

How would you do it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

北恋 2024-12-18 06:37:26

实现此目的的一种方法是使您的类型可配置,使其采用 TaskScheduler 实例。

public MyCollection(TaskScheduler scheduler) {
  this.taskFactory = new TaskFactory(scheduler);
}

public void Add(object x) {
  taskFactory.StartNew(() => {
    list.Add(x);
  });
}

现在,在单元测试中,您可以创建一个可测试的 TaskScheduler 版本。这是一个被设计为可配置的抽象类。简单地让计划函数将项目添加到队列中,然后添加一个函数来“立即”手动执行所有队列项目。然后您的单元测试可能类似于

var scheduler = new TestableScheduler();
var collection = new MyCollection(scehduler);
collection.Add(42);
scheduler.RunAll();
Assert.IsTrue(collection.Contains(42));

TestableScehduler 的示例实现

class TestableScheduler : TaskScheduler {
  private Queue<Task> m_taskQueue = new Queue<Task>();

  protected override IEnumerable<Task> GetScheduledTasks() {
    return m_taskQueue;
  }

  protected override void QueueTask(Task task) {
    m_taskQueue.Enqueue(task);
  }

  protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {
    task.RunSynchronously();
  }

  public void RunAll() {
    while (m_taskQueue.Count > 0) {
      m_taskQueue.Dequeue().RunSynchronously();
    }
  }
}

One way to do this is to make your type configurable such that it takes a TaskScheduler instance.

public MyCollection(TaskScheduler scheduler) {
  this.taskFactory = new TaskFactory(scheduler);
}

public void Add(object x) {
  taskFactory.StartNew(() => {
    list.Add(x);
  });
}

Now in your unit tests what you can do is create a testable version of TaskScheduler. This is an abstract class which is designed to be configurable. Simple have the schedule function add the items into a queue and then add a function to manually do all of the queue items "now". Then your unit test can look like this

var scheduler = new TestableScheduler();
var collection = new MyCollection(scehduler);
collection.Add(42);
scheduler.RunAll();
Assert.IsTrue(collection.Contains(42));

Example implementation of TestableScehduler

class TestableScheduler : TaskScheduler {
  private Queue<Task> m_taskQueue = new Queue<Task>();

  protected override IEnumerable<Task> GetScheduledTasks() {
    return m_taskQueue;
  }

  protected override void QueueTask(Task task) {
    m_taskQueue.Enqueue(task);
  }

  protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {
    task.RunSynchronously();
  }

  public void RunAll() {
    while (m_taskQueue.Count > 0) {
      m_taskQueue.Dequeue().RunSynchronously();
    }
  }
}
吾性傲以野 2024-12-18 06:37:26

对我有用的解决方案是将 TaskScheduler 作为我想要进行单元测试的代码的依赖项发送(例如,

MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler)

其中 asyncScheduler 用于调度在工作线程上运行的任务(阻止调用),而 guiScheduler 用于调度应运行的任务在 GUI 上(非阻塞调用),

然后在单元测试中,我将注入特定的调度程序,即 CurrentThreadTaskScheduler 实例。
CurrentThreadTaskScheduler 是一个调度程序实现,它立即运行任务,而不是对它们进行排队。

您可以在此处的 Microsoft 并行编程示例中找到实现。

我将粘贴代码以供快速参考:

/// <summary>Provides a task scheduler that runs tasks on the current thread.</summary>
public sealed class CurrentThreadTaskScheduler : TaskScheduler
{
    /// <summary>Runs the provided Task synchronously on the current thread.</summary>
    /// <param name="task">The task to be executed.</param>
    protected override void QueueTask(Task task)
    {
        TryExecuteTask(task);
    }

    /// <summary>Runs the provided Task synchronously on the current thread.</summary>
    /// <param name="task">The task to be executed.</param>
    /// <param name="taskWasPreviouslyQueued">Whether the Task was previously queued to the scheduler.</param>
    /// <returns>True if the Task was successfully executed; otherwise, false.</returns>
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return TryExecuteTask(task);
    }

    /// <summary>Gets the Tasks currently scheduled to this scheduler.</summary>
    /// <returns>An empty enumerable, as Tasks are never queued, only executed.</returns>
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return Enumerable.Empty<Task>();
    }

    /// <summary>Gets the maximum degree of parallelism for this scheduler.</summary>
    public override int MaximumConcurrencyLevel { get { return 1; } }
}

The solution that worked for me was to send the TaskScheduler as a dependency to the code I want to unit test (e.g.

MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler)

Where asyncScheduler is used to schedule tasks that run on worker threads (blocking calls) and guiScheduler is used to schedule tasks that should run on GUI (non blocking calls).

In the unit test, I would then inject a specific schedulers, i.e. CurrentThreadTaskScheduler instances.
CurrentThreadTaskScheduler is a scheduler implementation that runs the tasks immediately, instead of queuing them.

You can find the implementation in the Microsoft Samples for Parallel Programming here.

I'll paste the code for quick reference:

/// <summary>Provides a task scheduler that runs tasks on the current thread.</summary>
public sealed class CurrentThreadTaskScheduler : TaskScheduler
{
    /// <summary>Runs the provided Task synchronously on the current thread.</summary>
    /// <param name="task">The task to be executed.</param>
    protected override void QueueTask(Task task)
    {
        TryExecuteTask(task);
    }

    /// <summary>Runs the provided Task synchronously on the current thread.</summary>
    /// <param name="task">The task to be executed.</param>
    /// <param name="taskWasPreviouslyQueued">Whether the Task was previously queued to the scheduler.</param>
    /// <returns>True if the Task was successfully executed; otherwise, false.</returns>
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return TryExecuteTask(task);
    }

    /// <summary>Gets the Tasks currently scheduled to this scheduler.</summary>
    /// <returns>An empty enumerable, as Tasks are never queued, only executed.</returns>
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return Enumerable.Empty<Task>();
    }

    /// <summary>Gets the maximum degree of parallelism for this scheduler.</summary>
    public override int MaximumConcurrencyLevel { get { return 1; } }
}
逆流 2024-12-18 06:37:26

我和我的一位同事正在构建一个单元测试框架,它解决了 TPL 和 Rx 测试的问题,并且有您可以利用一个类来替换测试场景中的默认 TaskScheduler,这样您就不需要修改方法签名。该项目本身尚未发布,但您仍然可以在此处浏览该文件:

https://github.com/Testeroids/Testeroids/blob/master/solution/src/app/Testeroids/TplTestPlatformHelper.cs

设置任务调度程序的工作是在TplContextAspectAttribute.cs。

A colleague of mine and I are building a unit testing framework which addresses TPL and Rx testing, and there is a class which you could leverage to replace the default TaskScheduler in a testing scenario, so that you don't need to modify your method signatures. The project itself isn't published yet, but you can browse the file here nonetheless:

https://github.com/Testeroids/Testeroids/blob/master/solution/src/app/Testeroids/TplTestPlatformHelper.cs

The work of setting up the task scheduler is done in TplContextAspectAttribute.cs.

尘世孤行 2024-12-18 06:37:26

为列表创建一个公共财产怎么样?

public ConcurrentList<object> List { get; set; }

或者在调试构建时将其设为公共字段:

#if DEBUG
public static ConcurrentList<object> list = new ConcurrentList<object>();
#else
private static ConcurrentList<object> list = new ConcurrentList<object>();
#endif

What about making a public property for the list?

public ConcurrentList<object> List { get; set; }

or maybe make it a public field when in DEBUG build:

#if DEBUG
public static ConcurrentList<object> list = new ConcurrentList<object>();
#else
private static ConcurrentList<object> list = new ConcurrentList<object>();
#endif
如歌彻婉言 2024-12-18 06:37:26

至少对于大多数简单的情况,我喜欢对此类事情使用“过期”断言。例如:

YourCollection sut = new YourCollection();

object newItem = new object();
sut.Add(newItem);

EventualAssert.IsTrue(() => sut.Contains(newItem), TimeSpan.FromSeconds(2));

其中 EventualAssert.IsTrue() 看起来像这样:

public static void IsTrue(Func<bool> condition, TimeSpan timeout)
{
    if (!SpinWait.SpinUntil(condition, timeout))
    {
        Assert.IsTrue(condition());
    }
}

我通常还会添加一个具有默认超时的覆盖,我将其用于大多数测试,但是 ymmv.. 。

For at least most simple-ish cases, I like to use an "expiring" assertion for this sort of thing. e.g.:

YourCollection sut = new YourCollection();

object newItem = new object();
sut.Add(newItem);

EventualAssert.IsTrue(() => sut.Contains(newItem), TimeSpan.FromSeconds(2));

where EventualAssert.IsTrue() looks something like this:

public static void IsTrue(Func<bool> condition, TimeSpan timeout)
{
    if (!SpinWait.SpinUntil(condition, timeout))
    {
        Assert.IsTrue(condition());
    }
}

I also generally add an override with a default timeout which I use for most of my tests, but ymmv...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文