2011-10-21 5 views
18

Immaginate una funzione come questa:Come scrivere unit test con TPL e TaskScheduler

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

non mi importa quando esattamente è aggiunto il fentry alla lista, ma ho bisogno di essere aggiunto alla fine (ovviamente;))

Non vedo un modo per disinstallare correttamente le cose come questa senza restituire alcun gestore di callback o sth. e quindi aggiungere la logica che non è richiesta per il programma

Come lo faresti?

+1

Cosa stai implementando 'ConcurrentList ' da – msarchet

risposta

17

Un modo per fare ciò è rendere il tipo configurabile in modo tale da richiedere un'istanza TaskScheduler.

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

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

Ora nella vostra unità mette alla prova ciò che si può fare è creare una versione testabile di TaskScheduler. Questa è una classe astratta progettata per essere configurabile. Semplice è la funzione di pianificazione aggiungere gli elementi in una coda e quindi aggiungere una funzione per eseguire manualmente tutti gli elementi della coda "ora". Allora la vostra unit test può assomigliare a questo

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

esempio di implementazione 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

Non sei preoccupato di nascondere le potenziali condizioni di gara usando un approccio come questo? O ritieni che testare il comportamento multithread del codice non rientri nell'ombrello del test unitario? –

+1

@NicoleCalinoiu questo approccio è solo per testare il caso specifico che quando l'attività X completa il risultato è nella raccolta. Per verificare le condizioni di gara userei una versione molto diversa di questo tipo. Entrambi i casi devono essere testati di sicuro. – JaredPar

+0

Indovina hai ragione ... speravo che esistesse una "soluzione più semplice" rispetto a DI tutto collegato al thread .. :( – David

0

Che dire di fare una proprietà pubblica per la lista?

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

o forse ne fanno un campo pubblico quando in debug:

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

Per i casi almeno la maggior parte semplici-ish, mi piace usare una "scadenza" asserzioni per questo genere di cose. ad es.:

YourCollection sut = new YourCollection(); 

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

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

dove EventualAssert.IsTrue() sembra qualcosa di simile:

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

ho anche generalmente aggiungo un override con un timeout predefinito che io uso per la maggior parte dei miei test, ma YMMV ...

6

La soluzione che ha funzionato per me era inviare TaskScheduler come dipendenza dal codice che voglio testare (es.

MyClass(TaskScheduler asyncScheduler, TaskScheduler guiScheduler) 

Dove asyncScheduler viene utilizzato per pianificare attività eseguite su thread di lavoro (blocco chiamate) e guiScheduler viene utilizzato per pianificare attività che devono essere eseguite sulla GUI (chiamate non bloccanti).

Nel test dell'unità, avrei quindi iniettato uno schedulatore specifico, ad esempio istanze CurrentThreadTaskScheduler. CurrentThreadTaskScheduler è un'implementazione dello scheduler che esegue immediatamente le attività, invece di metterle in coda.

È possibile trovare l'implementazione in Microsoft Samples per la programmazione parallela here.

io incollare il codice di riferimento rapido:

/// <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 sovrascrivere manualmente l'utilità di pianificazione predefinita in TPL per evitare di introdurre questa dipendenza nel codice: http://www.jarrodstormo.com/2012/02/05/unit-testing-when-using-the-tpl/ – Dunc

0

Un mio collega ed io stiamo costruendo un unit testing framework che affronta TPL e Rx test, e non v'è una classe che si potrebbe sfruttare per sostituire il TaskScheduler predefinito in uno scenario di test, in modo che non sia necessario modificare le firme del metodo. Il progetto in sé non è ancora pubblicato, ma è possibile sfogliare il file qui comunque:

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

Il lavoro di istituire il task scheduler è fatto in TplContextAspectAttribute.cs.