2011-10-21 1454 views
18

Imagínese una función como esta:Cómo escribir pruebas unitarias con TPL y TaskScheduler

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

no me importa cuándo exactamente se añade el Fentry a la lista, pero necesito que se agregue al final (obviamente;))

No veo una manera de probar adecuadamente cosas como esta sin devolver ningún controlador de devolución de llamada o algo así. y por lo tanto, agregar lógica que no es necesaria para el programa

¿Cómo lo haría?

+1

¿Qué está implementando 'ConcurrentList ' de – msarchet

Respuesta

17

Una forma de hacer esto es hacer que su tipo sea configurable de manera que tome una instancia de TaskScheduler.

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

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

Ahora en su unidad pone a prueba lo que puede hacer es crear una versión contrastable de TaskScheduler. Esta es una clase abstracta que está diseñada para ser configurable. Simple, haga que la función de programación agregue los elementos a una cola y luego agregue una función para hacer manualmente todos los elementos de la cola "ahora". Luego, su unidad de prueba puede tener este aspecto

var scheduler = new TestableScheduler(); 
var collection = new MyCollection(scehduler); 
collection.Add(42); 
scheduler.RunAll(); 
Assert.IsTrue(collection.Contains(42)); 

ejemplo de implementación de 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

¿No le preocupa ocultar las posibles condiciones de carrera utilizando un enfoque como este? ¿O cree que probar el comportamiento de código multiproceso no encaja bajo el paraguas de prueba de unidad? –

+1

@NicoleCalinoiu este enfoque solo sirve para probar el caso específico de que cuando la tarea X completa el resultado está en la colección. Para evaluar las condiciones de carrera, usaría una versión muy diferente de este tipo. Ambos casos deben ser probados con seguridad. – JaredPar

+0

Supongo que tienes razón ... esperaba que hubiera una "solución más simple" que DI todo lo relacionado con hilos ... :( – David

0

Lo que trata de hacer una propiedad pública para la lista?

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

o tal vez lo convierten en un campo público cuando en generación de depuración:

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

Para los casos al menos la mayoría simples-ish, me gusta usar una "expira" afirmación de este tipo de cosas. p.:

YourCollection sut = new YourCollection(); 

object newItem = new object(); 
sut.Add(newItem); 

EventualAssert.IsTrue(() => sut.Contains(newItem), TimeSpan.FromSeconds(2)); 

donde EventualAssert.IsTrue() se ve algo como esto:

public static void IsTrue(Func<bool> condition, TimeSpan timeout) 
{ 
    if (!SpinWait.SpinUntil(condition, timeout)) 
    { 
     Assert.IsTrue(condition()); 
    } 
} 

también suelen añadir una anulación con un tiempo de espera predeterminado que utilizo para la mayoría de mis pruebas, pero tu caso es distinto ...

6

La solución que funcionó para mí fue enviar el TaskScheduler como una dependencia del código que quiero probar de la unidad (por ej.

MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler) 

Donde asyncScheduler se utiliza para programar tareas que se ejecutan en subprocesos de trabajo (llamadas de bloqueo) y guiScheduler se utiliza para programar tareas que deben ejecutarse en la GUI (llamadas no bloqueadas).

En la prueba unitaria, me gustaría inyectar un planificador específico, es decir instancias de CurrentThreadTaskScheduler. CurrentThreadTaskScheduler es una implementación del planificador que ejecuta las tareas de forma inmediata, en lugar de ponerlas en cola.

Puede encontrar la implementación en las muestras de Microsoft para la programación paralela here.

voy a pegar el código de referencia rápida:

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

. ... o anule manualmente el planificador de tareas predeterminado en TPL para evitar introducir esta dependencia en su código: http://www.jarrodstormo.com/2012/02/05/unit-testing-when-using-the-tpl/ – Dunc

0

Un colega mío y yo estamos construyendo un unit testing framework que se ocupa de las pruebas de TPL y Rx, y hay una clase que se podría aprovechar para reemplazar el TaskScheduler por defecto en un escenario de prueba, para que no necesite modificar sus firmas de métodos. El proyecto en sí no se ha publicado todavía, pero usted puede navegar por el archivo aquí, sin embargo:

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

la obra de la creación de la programador de tareas se realiza en TplContextAspectAttribute.cs.