【问题标题】:How to write unit tests with TPL and TaskScheduler如何使用 TPL 和 TaskScheduler 编写单元测试
【发布时间】:2011-10-21 17:20:54
【问题描述】:

想象一个这样的函数:

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

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

我看不到在不返回任何回调处理程序或某事的情况下正确地对此类内容进行单元测试的方法。并因此添加程序不需要的逻辑

你会怎么做?

【问题讨论】:

  • 你在实施什么ConcurrentList&lt;object&gt; from

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


【解决方案1】:

做到这一点的一种方法是使您的类型可配置,以便它采用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();
    }
  }
}

【讨论】:

  • 您不担心使用这样的方法隐藏潜在的竞争条件吗?还是您觉得测试代码的多线程行为不适合单元测试?
  • @NicoleCalinoiu 这种方法仅用于测试任务 X 完成时结果在集合中的特定情况。为了测试竞争条件,我会使用这种类型的一个非常不同的版本。这两种情况都需要进行测试。
  • 猜你是对的......我希望有一个“更简单的解决方案”,而不是 DI 所有与线程相关的东西...... :(
  • @JaredPar 我尝试了您的 TaskScheduler 但它不起作用(TryXxx 方法必须返回一个不起作用的布尔值)但真正的问题是我获得了 InvalidOperationException "RunSynchronously 可能不会被调用一个已经开始的任务。”。
  • 原理不错,但这个实现实际上行不通。
【解决方案2】:

对我有用的解决方案是将 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; } }
}

【讨论】:

  • ... 或手动覆盖 TPL 中的默认任务调度程序以避免在您的代码中引入这种依赖关系:jarrodstormo.com/2012/02/05/unit-testing-when-using-the-tpl
  • 任何采用这种方法的人都可能会考虑注入一些更可扩展的东西,例如ISchedulerService 其中还可以承载其他类型的调度器。我注入了这个接口,其中包含 Rx 的调度程序以及 TPL 调度程序,以便我可以在我的代码中使用它们。
【解决方案3】:

为列表创建一个公共属性怎么样?

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

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-02-03
        • 2019-01-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-07-25
        相关资源
        最近更新 更多