如何使用TPL和TaskScheduler编写unit testing

想象一下这样的函数:

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

我不在乎什么时候确实将fentry添加到列表中,但是我需要将它添加到最后(显然;))

我没有看到一种方法来正确地unit testing这样的东西而不返回任何回调处理程序或某事。 因此,添加程序不需要的逻辑

你会怎么做?

一种方法是使您的类型可配置,以便它需要一个TaskScheduler实例。

 public MyCollection(TaskScheduler scheduler) { this.taskFactory = new TaskFactory(scheduler); } public void Add(object x) { taskFactory.StartNew(() => { list.Add(x); }); } 

现在,在您的unit testing中,您可以创建一个可测试版本的TaskScheduler 。 这是一个可配置的抽象类。 简单地使用schedule函数将项添加到队列中,然后添加一个函数以“现在”手动执行所有队列项。 然后你的unit testing看起来像这样

 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 m_taskQueue = new Queue(); protected override IEnumerable 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(); } } } 

对我有用的解决方案是将TaskScheduler作为依赖项发送到我想要进行unit testing的代码(例如

 MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler) 

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

在unit testing中,我会注入一个特定的调度程序,即CurrentThreadTaskScheduler实例。 CurrentThreadTaskScheduler是一个调度程序实现,它可以立即运行任务,而不是对它们进行排队。

您可以在此处找到Microsoft Samples for Parallel Programming中的实现。

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

 /// Provides a task scheduler that runs tasks on the current thread. public sealed class CurrentThreadTaskScheduler : TaskScheduler { /// Runs the provided Task synchronously on the current thread. /// The task to be executed. protected override void QueueTask(Task task) { TryExecuteTask(task); } /// Runs the provided Task synchronously on the current thread. /// The task to be executed. /// Whether the Task was previously queued to the scheduler. /// True if the Task was successfully executed; otherwise, false. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return TryExecuteTask(task); } /// Gets the Tasks currently scheduled to this scheduler. /// An empty enumerable, as Tasks are never queued, only executed. protected override IEnumerable GetScheduledTasks() { return Enumerable.Empty(); } /// Gets the maximum degree of parallelism for this scheduler. public override int MaximumConcurrencyLevel { get { return 1; } } } 

如何为列表制作公共财产?

 public ConcurrentList List { get; set; } 

或者在DEBUG构建时使其成为公共字段:

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

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

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

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

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

 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 condition, TimeSpan timeout) { if (!SpinWait.SpinUntil(condition, timeout)) { Assert.IsTrue(condition()); } } 

我通常也会添加一个带有默认超时的覆盖,我会在大多数测试中使用它,但是ymmv …