2016-05-16 107 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