等待单元测试中所有任务完成

19

我有一个类需要进行单元测试:

public class SomeClass
{
    public void Foo()
    {
        Bar();
    }

    private void Bar()
    {
        Task.Factory.StartNew(() =>
        {
            // Do something that takes some time (e.g. an HTTP request)
        });
    }
}

这是我的单元测试的样子:

[TestMethod]
public void TestFoo()
{
    // Arrange
    var obj = new SomeClass();

    // Act
    obj.Foo();
    obj.Foo();
    obj.Foo();

    // Assert
    /* I need something to wait on all tasks to finish */
    Assert.IsTrue(...);
}

因此,在开始我的断言之前,我需要使单元测试线程等待在Bar方法中启动的所有任务完成其工作。

重要提示:我不能更改SomeClass

我该怎么做?


1
很遗憾,你的设计有问题。你不应该在没有返回调用者等待的东西的情况下生成新任务。这是特别棘手的,因为托管线程池的线程(在其中执行你的任务)被标记为后台线程,这意味着如果应用程序的前台线程终止,你的任务可能会在完成之前被中止。 - Douglas
我知道,但不幸的是我无法改变它,而且上面的示例只是我所面临的复杂情况的简化。 - mmutilva
3个回答

33

解决这个问题的一种方法是自定义任务调度器,以便您可以跟踪嵌套任务的完成情况。例如,您可以定义一个同步执行任务的调度程序,如下所示:

class SynchronousTaskScheduler : TaskScheduler
{
    protected override void QueueTask(Task task)
    {
        this.TryExecuteTask(task);
    }

    protected override bool TryExecuteTaskInline(Task task, bool wasPreviouslyQueued)
    {
        return this.TryExecuteTask(task);
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        yield break;
    }
}

随后,创建此同步任务计划程序的一个实例,并使用它来执行一个根任务,该任务又会生成所有“隐藏”任务。由于嵌套任务从其父任务继承当前任务计划程序,因此所有内部任务也将在我们的同步调度程序上运行,意味着我们最外层的StartNew调用仅在所有任务完成时返回。

TaskScheduler scheduler = new SynchronousTaskScheduler();

Task.Factory.StartNew(() =>
{
    // Arrange
    var obj = new SomeClass();

    // Act
    obj.Foo();
    obj.Foo();
    obj.Foo();
}, 
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler);

// Assert
/* I need something to wait on all tasks to finish */
Assert.IsTrue(...);
这种方法的缺点是你会失去所有任务的并发性;不过,你可以通过增强自定义调度程序来解决这个问题,使其成为一个既并发又允许你跟踪正在执行的任务的调度程序。

使用这种方法的一个劣势是您将失去所有任务的并发性;但是,您可以通过增强自定义调度器来解决此问题,使其成为既并发又允许您跟踪正在执行的任务的调度器。


1
这个答案可能为我节省了数小时的时间。在事先找到信息方面真是太棒了。谢谢! - juhan_h
我无法在特定情况下让您的SynchronousTaskScheduler工作,我在这里发布了问题:https://dev59.com/pITba4cB1Zd3GeqP6HTw - quadroid
1
如果您的隐藏任务是使用Task.Run而不是Task.Factory.StartNew启动的,则无法正常工作。 - JustAMartin
3
@JustAMartin:没错。Task.Run 相当于 Task.Factory.StartNew(…, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default),因此嵌套的任务不会从其父任务继承当前任务调度程序。 - Douglas

6
Task.WaitAll(the, list, of, task, objects, you, need, to, wait, on);

如果这是一个void async方法,那么你就无法实现它。这个设计有问题。它们只适用于“点火并忘记”的情况。

4
很不幸,@Toto在他的单元测试中无法获得任务对象。 - Hemario
1
OP问题中没有涉及异步操作。 - quadroid

3

不确定你是否被允许进行此更改,但我通过以下方式使它工作:

namespace ParallelProgramming.Playground
{
    public class SomeClass
    {
        public Task Foo()
        {
            return Bar();
        }

        private static Task Bar()
        {
            return Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("I fired off. Thread ID: {0}", Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(5000);
                    return true; //or whatever else you want.
                });
        }
    }

    [TestClass]
    public class StackOverflow
    {
        [TestMethod]
        public void TestFoo()
        {
            // Arrange
            var obj = new SomeClass();

            var results = new ConcurrentBag<Task>(); 
            var waitForMe = Task.Factory.StartNew(() =>
                {
                    // Act
                    results.Add(obj.Foo());
                    results.Add(obj.Foo());
                    results.Add(obj.Foo());

                    return true;
                });


            Task.WaitAll(waitForMe);

            // Assert
            /* I need something to wait on all tasks to finish */
            Assert.IsTrue(waitForMe.Result);
            Assert.AreEqual(3, results.Count);
        }
    }
}

实际上,如果你正在使用 TPL 中的 Thread,那么它已经成为了遗留代码。因此它并不是非常必要的。 - hackp0int
该线程并未被建议用于回答问题。我只是利用它来给方法一些加载时间,因为它除了Console.Writeline什么也没做。 - Cubicle.Jockey
我在Stephen Cleary的书中读到,如果你在TPL应用程序中使用Thread,它已经成为了“遗留代码”。只是觉得值得一提,伙计。 - hackp0int
@IamStalker 我同意你的看法,这是一个非常好的提示。我只希望那些参考这个例子的人能够明白这不是必需品。:) 但无论如何,指出这一点非常有价值。 - Cubicle.Jockey
你也可以调用 waitForMe.Wait() 而不是 WaitAll,我认为这样更加整洁。 - Wayne Koorts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接