Как написать модульные тесты с помощью TPL и TaskScheduler
Представьте себе такую функцию:
private static ConcurrentList<object> list = new ConcurrentList<object>();
public void Add(object x)
{
Task.Factory.StartNew(() =>
{
list.Add(x);
}
}
Мне все равно, КОГДА именно в этот список добавляется шпага, но мне нужно, чтобы он был добавлен в конце (очевидно;))
Я не вижу способ правильно убрать такие вещи, не возвращая никакого обработчика обратного вызова или sth. и, следовательно, добавление логики, которая не требуется для программы
Как вы это сделаете?
Ответы
Ответ 1
Один из способов сделать это - настроить ваш тип таким образом, чтобы он выполнял экземпляр TaskScheduler
.
public MyCollection(TaskScheduler scheduler) {
this.taskFactory = new TaskFactory(scheduler);
}
public void Add(object x) {
taskFactory.StartNew(() => {
list.Add(x);
});
}
Теперь в вашем модульном тестировании вы можете создать тестовую версию TaskScheduler
. Это абстрактный класс, который предназначен для конфигурирования. Простая функция расписания добавляет элементы в очередь, а затем добавляет функцию, чтобы вручную выполнять все элементы очереди "сейчас". Тогда ваш unit test может выглядеть следующим образом:
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();
}
}
}
Ответ 2
Решение, которое работало для меня, заключалось в том, чтобы отправить TaskScheduler в зависимость от кода, который я хочу использовать unit test (например,
MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler)
Где asyncScheduler используется для планирования задач, выполняемых в рабочих потоках (блокирование вызовов), а guiScheduler используется для планирования задач, которые должны выполняться в графическом интерфейсе (неблокирующие вызовы).
В unit test я бы тогда ввел конкретные планировщики, то есть экземпляры CurrentThreadTaskScheduler.
CurrentThreadTaskScheduler - это реализация планировщика, которая сразу запускает задачи, а не ставит их в очередь.
Вы можете найти реализацию в Microsoft Samples for Parallel Programming здесь.
Я скоро вставлю код:
/// <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; } }
}
Ответ 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
Мой коллега и я создаем модульную систему тестирования, которая рассматривает тестирование 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...