2016-05-16 46 views
1

我的應用程序中有一個線程模塊,用於管理啓動並行操作。它增加了各種時間和日誌記錄的原因,並且很複雜,我最近發現我編寫了一個錯誤代碼,它在雙重嵌套的線程上啓動了一些任務。單元測試線程代碼創建的線程數

即它在呼喚等價的:一方面是

Task.Run(
    async() => await Task.Run(
     () => DoStuff(); 
    ); 
).Wait() 

現在,代碼工作...目標代碼獲取運行,並等待代碼不會繼續下去,直到目標代碼有完成。

另一方面,它使用2個線程來做到這一點,而不是1線程,因爲我們遇到線程匱乏問題,這是一個問題。

我知道如何修復代碼,但我想編寫一個單元測試以確保A)我修復了所有這些錯誤/修復了它在所有場景中的錯誤。 B)沒有人在未來重新創建這個bug。

但我看不到如何獲得「我創建的所有線程」。 CurrentProcess.Threads給了我線程的LOADS,並沒有明顯的方式來確定哪些是我關心的。

有什麼想法?

回答

2

由於通常是單元測試涉及靜態方法(本例中爲Task.Run)的解決方案,因此您可能需要將某些內容作爲依賴項傳遞給您的類,將其包裝起來,然後可以添加行爲在測試中。

正如@Rich在他的回答中所建議的,你可以通過傳遞TaskScheduler來實現。然後,您的測試版本可以在任務排入隊列時保持其計數。

製作測試TaskScheduler實際上是因爲保護級別有點難看,但在這個崗位的底部我已經包含了一個包裝現有TaskScheduler(例如,你可以使用TaskScheduler.Default)。

不幸的是,你還需要像

Task.Run(() => DoSomething); 

改變你的來電像

Task.Factory.StartNew(
    () => DoSomething(), 
    CancellationToken.None, 
    TaskCreationOptions.DenyChildAttach, 
    myTaskScheduler); 

這是basically what Task.Run does under the hood,除了與TaskScheduler.Default。你當然可以將它包裝在某處的輔助方法中。

或者,如果你不嬌氣有關在您的測試代碼中的一些風險較高的反射,你可以劫持TaskScheduler.Default屬性,因此,您仍然可以只使用Task.Run

var defaultSchedulerField = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.Static | BindingFlags.NonPublic); 
var scheduler = new TestTaskScheduler(TaskScheduler.Default); 
defaultSchedulerField.SetValue(null, scheduler); 

(私人字段名是從TaskScheduler.cs line 285。 )

因此,例如,本次測試將通過使用下面我TestTaskScheduler和反射招:

[Test] 
public void Can_count_tasks() 
{ 
    // Given 
    var originalScheduler = TaskScheduler.Default; 
    var defaultSchedulerField = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.Static | BindingFlags.NonPublic); 
    var testScheduler = new TestTaskScheduler(originalScheduler); 
    defaultSchedulerField.SetValue(null, testScheduler); 

    // When 
    Task.Run(() => {}); 
    Task.Run(() => {}); 
    Task.Run(() => {}); 

    // Then 
    testScheduler.TaskCount.Should().Be(3); 

    // Clean up 
    defaultSchedulerField.SetValue(null, originalScheduler); 
} 

下面是測試任務調度程序:

using System.Collections.Generic; 
using System.Reflection; 
using System.Threading.Tasks; 

public class TestTaskScheduler : TaskScheduler 
{ 
    private static readonly MethodInfo queueTask = GetProtectedMethodInfo("QueueTask"); 
    private static readonly MethodInfo tryExecuteTaskInline = GetProtectedMethodInfo("TryExecuteTaskInline"); 
    private static readonly MethodInfo getScheduledTasks = GetProtectedMethodInfo("GetScheduledTasks"); 

    private readonly TaskScheduler taskScheduler; 

    public TestTaskScheduler(TaskScheduler taskScheduler) 
    { 
     this.taskScheduler = taskScheduler; 
    } 

    public int TaskCount { get; private set; } 

    protected override void QueueTask(Task task) 
    { 
     TaskCount++; 
     CallProtectedMethod(queueTask, task); 
    } 

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) 
    { 
     return (bool)CallProtectedMethod(tryExecuteTaskInline, task, taskWasPreviouslyQueued); 
    } 

    protected override IEnumerable<Task> GetScheduledTasks() 
    { 
     return (IEnumerable<Task>)CallProtectedMethod(getScheduledTasks); 
    } 

    private object CallProtectedMethod(MethodInfo methodInfo, params object[] args) 
    { 
     return methodInfo.Invoke(taskScheduler, args); 
    } 

    private static MethodInfo GetProtectedMethodInfo(string methodName) 
    { 
     return typeof(TaskScheduler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); 
    } 
} 

或收拾使用RelflectionMagic由@hgcummings在評論中建議:

var scheduler = new TestTaskScheduler(TaskScheduler.Default); 
typeof(TaskScheduler).AsDynamicType().s_defaultTaskScheduler = scheduler; 
using System.Collections.Generic; 
using System.Threading.Tasks; 
using ReflectionMagic; 

public class TestTaskScheduler : TaskScheduler 
{ 
    private readonly dynamic taskScheduler; 

    public TestTaskScheduler(TaskScheduler taskScheduler) 
    { 
     this.taskScheduler = taskScheduler.AsDynamic(); 
    } 

    public int TaskCount { get; private set; } 

    protected override void QueueTask(Task task) 
    { 
     TaskCount++; 
     taskScheduler.QueueTask(task); 
    } 

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) 
    { 
     return taskScheduler.TryExecuteTaskInline(task, taskWasPreviouslyQueued); 
    } 

    protected override IEnumerable<Task> GetScheduledTasks() 
    { 
     return taskScheduler.GetScheduledTasks(); 
    } 
} 
+0

多數民衆贊成...驚人?可怕的?我真的不明白爲什麼「使用這個作爲默認調度程序」不是你可以JustDo的東西,但很高興知道如何去做。 – Brondahl

+1

不錯(或者可能是邪惡的)!你可以使用ReflectionMagic的AsDynamic來簡化TestTaskRunner(雖然可能會有性能問題)。請參閱https://blogs.msdn.microsoft.com/davidebb/2010/01/18/use-c-4-0-dynamic-to-drastically-simplify-your-private-reflection-code/ – hgcummings

+1

很好。我會添加一些清理代碼,以便在測試結束時再次將TaskScheduler.Default返回,否則您可能會對其他稍後的測試產生不必要的干擾。 – Rich

2

如何讓

Task.Run不會產生任何線索「我已經創建的所有線程」抓住。它會安排作業在當前配置的線程池上運行。見https://msdn.microsoft.com/library/system.threading.tasks.taskscheduler.aspx

如果你的意思是「怎麼算我入隊的任務數」,我想你會需要創建的TaskScheduler自定義實現其計算進入任務和配置您的測試代碼中使用它。在上面鏈接的頁面上顯示了一個自定義TaskScheduler的示例。

+0

是的,你是正確的..這正是我的意思。我想你不知道該怎麼做? /我用一個新的短語去谷歌 – Brondahl