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

时间:2011-10-21 17:20:55

标签: c# unit-testing asynchronous task-parallel-library

想象一下这样的函数:

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

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

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

你会怎么做?

5 个答案:

答案 0 :(得分:18)

执行此操作的一种方法是使您的类型可配置,使其占用TaskScheduler个实例。

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

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

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

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();
    }
  }
}

答案 1 :(得分:7)

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

MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler)

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

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

您可以在Microsoft Samples for Parallel Programming here中找到该实现。

我会粘贴代码以便快速参考:

/// <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; } }
}

答案 2 :(得分:0)

为列表制作公共财产怎么样?

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

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

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

答案 3 :(得分:0)

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

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

设置任务计划程序的工作在TplContextAspectAttribute.cs完成。

答案 4 :(得分:-1)

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

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 ......