Как написать модульные тесты с TPL и TaskScheduler - PullRequest
18 голосов
/ 21 октября 2011

Представьте себе функцию, подобную этой:

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

Мне все равно, когда именно список добавляется в список, но мне нужно добавить его в конце (очевидно;))

Я не вижу способа правильно протестировать подобные вещи без возврата какого-либо обработчика обратного вызова или sth.и для этого добавить логику, которая не требуется для программы

Как бы вы это сделали?

Ответы [ 5 ]

18 голосов
/ 21 октября 2011

Один из способов сделать это - настроить ваш тип так, чтобы он принимал экземпляр 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();
    }
  }
}
7 голосов
/ 22 января 2013

Решение, которое работало для меня, состояло в том, чтобы отправить TaskScheduler в качестве зависимости к коду, который я хочу выполнить модульным тестом (например,

MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler)

Где asyncScheduler используется для планирования задач, выполняемых в рабочих потоках (блокирование вызовов), а guiScheduler - для планирования задач, которые должны выполняться в графическом интерфейсе (не блокирующие вызовы).

В модульном тесте я бы вводил определенные планировщики, то есть экземпляры 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; } }
}
0 голосов
/ 19 июня 2013

Мой коллега и я строим инфраструктуру для модульного тестирования , которая занимается TPL и Rx-тестированием, и есть класс, который вы можете использовать для замены TaskScheduler по умолчанию в сценарии тестирования, чтобы вы моглине нужно изменять сигнатуры вашего метода.Сам проект еще не опубликован, но вы можете просмотреть файл здесь:

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

Работа по настройке планировщика задач выполняется в TplContextAspectAttribute.cs.

0 голосов
/ 21 октября 2011

По крайней мере, для большинства простых случаев я бы хотел использовать утверждение «истекающий» для такого рода вещей. , например * * тысяча две: * +1003 * 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 ...

0 голосов
/ 21 октября 2011

Как насчет создания публичной собственности для списка?

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