How to write unit tests with TPL and TaskScheduler

后端 未结 5 1552
离开以前
离开以前 2021-02-01 06:50

Imagine a function like this:

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


        
                      
相关标签:
5条回答
  • 2021-02-01 07:14

    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.

    0 讨论(0)
  • 2021-02-01 07:16

    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; } }
    }
    
    0 讨论(0)
  • 2021-02-01 07:17

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

    0 讨论(0)
  • 2021-02-01 07:28

    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
    
    0 讨论(0)
  • 2021-02-01 07:31

    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();
        }
      }
    }
    
    0 讨论(0)
提交回复
热议问题