【问题标题】:Unit testing code that uses Task.Factory.StartNew().ContinueWith()使用 Task.Factory.StartNew().ContinueWith() 的单元测试代码
【发布时间】:2012-10-29 19:41:46
【问题描述】:

所以我有一些代码

Task.Factory.StartNew(() => this.listener.Start()).ContinueWith(
                    (task) =>
                        {
                            if (task.IsCompleted)
                            {
                                this.status = WorkerStatus.Started;
                                this.RaiseStatusChanged();
                                this.LogInformationMessage("Worker Started.");
                            }
                        });

当我测试时,我正在模拟所有依赖对象(namley this.listener.Start())。问题是测试在调用 ContinueWith 之前完成执行。当我调试时,由于我单步执行代码的额外延迟,它被调用得很好。

那么我如何 - 从不同程序集中的测试代码 - 确保代码在我的测试达到其断言之前运行?

我可以只使用 Thread.Sleep ...但这似乎是一种非常老套的方法。

我想我正在寻找 Thread.Join 的 Task 版本。

【问题讨论】:

  • 怎么完成的,有没有报错?
  • 没有错误... listener.start 被模拟并完成.. 我可以在调试模式下验证。这是一个时间问题
  • 我会说这是一个同步问题,你需要从你调用的异步方法中获得一些反馈
  • 是的,我同意,任务的包装似乎是答案。它只是到那时我开始觉得我正在实现与线程池已经可以做的非常相似的事情。应该坚持我所知道的哈哈。
  • 为了他人的利益,请参阅codeproject.com/Tips/706553/Mocking-Task-Factory-StartNew

标签: c# unit-testing nunit moq task-parallel-library


【解决方案1】:

如果有任何方法可以在处理结束时通知您(您可以为该 StatusChanged 事件添加处理程序吗?),请使用 ManualResetEvent 并在合理的超时时间内等待它。如果超时过期,则测试失败,否则继续执行您的断言。

例如

var waitHandle = new ManualResetEvent(false);
sut.StatusChanged += (s, e) => waitHandle.Set();

sut.DoStuff();

Assert.IsTrue(waitHandle.WaitOne(someTimeout), "timeout expired");
// do asserts here

【讨论】:

  • 我喜欢开箱即用的思维方式。但不能帮助我解决一般问题!
  • 帮助他理解。您正在考虑将 Thread.Sleep 作为替代方案。这有点像这样,但如果你能收到通知,它会在处理结束后继续。与 Thread.Join 等效的是 Task.Wait,但您已声明不会在您正在测试的方法中公开任务。
  • 完全正确。也许我已经设置了一个不可能的情况。在这种情况下,我会将您标记为答案 ;) ... 现在的一般答案似乎是围绕任务内容编写某种包装器,以便我可以做我需要的事情。我真的在检查我不会重新发明轮子
【解决方案2】:

无论初始任务是否在ContinueWith() 调用之前完成,后续任务仍将运行。我仔细检查了以下内容:

// Task immediately exits
var task = Task.Factory.StartNew(() => { });

Thread.Sleep(100);

// Continuation on already-completed task
task.ContinueWith(t => { MessageBox.Show("!"); });

进一步调试。也许你的任务失败了。

【讨论】:

  • 这不是我要问的。有趣的是你可以做到这一点,虽然我没有意识到你可以添加延续作为事后的想法!我的任务不会失败,因为它被嘲笑并且什么都不做......实际上我正在执行与您相同的事情,但通过 Moq 代理。 thread.sleep 也正是我试图避免做的事情。问题是任务运行和继续运行之间存在延迟,并且该延迟比运行测试的其余部分要长。我想做与显式线程相同的操作,然后等待完成并重新加入它们。
【解决方案3】:

考虑以下几点:

public class SomeClass
{
    public void Foo()
    {
        var a = new Random().Next();
    }
}

public class MyUnitTest
{
    public void MyTestMethod()
    {
        var target = new SomeClass();        
        target.Foo(); // What to assert, what is the result?..
    }
}

分配给a 的值是什么?你无法判断,除非结果在方法Foo() 之外返回(作为返回值、公共属性、事件等)。

coordinating the actions of threads for a predictable outcome”的进程被称为Synchronization

在您的情况下,最简单的解决方案之一可能是返回 Task 类的实例并使用其 Wait() 方法:

var task = Task.Factory.StartNew(() => Method1())
    .ContinueWith(() => Method2());

无需等待第一个任务,因为ContinueWith() 创建了一个异步执行的延续当目标任务完成时MSDN):

task.Wait();

【讨论】:

  • 我认为您误解了他的问题(或者我可能是?)。他的延续任务显然没有执行,因为初始任务在ContinueWith 方法可以被调用之前完成。
  • @davenewza 无需等待第一个任务,因为ContinueWith() 创建了一个在目标任务完成时异步执行的延续
  • 我无法从我的测试中访问任务实例,我宁愿不为此目的编写一个包装器(但也许我必须这样做)。
【解决方案4】:

我认为没有一种简单而实用的方法可以做到这一点。我自己刚刚遇到了同样的问题,而 Thread.Sleep(X) 是迄今为止解决问题的最简单(如果不是优雅的话)的方法。

我考虑的唯一其他解决方案是将 Task.Factory.StartNew() 调用隐藏在可以从测试中模拟的接口后面,从而完全在测试场景中删除任务的实际执行(但仍然有一个期望接口方法将被调用。例如:

public interface ITaskWrapper
{
    void TaskMethod();
}

以及你的具体实现:

public class MyTask : ITaskWrapper
{
    public void TaskMethod()
    {
        Task.Factory.StartNew(() => DoSomeWork());
    }
}

然后只需在您的测试方法中模拟 ITaskWrapper 并设置期望 TaskMethod 被调用。

【讨论】:

【解决方案5】:

在使用响应式扩展的被测代码期间处理异步进程时,一种方法是使用 TestScheduler。 TestScheduler 可以及时向前移动,耗尽所有已调度的任务等。因此,您的测试代码可以采用 IScheduler,您可以为其提供 TestScheduler 实例。然后,您的测试可以操纵时间,而无需实际休眠、等待或同步。这种方法的改进是Lee Campbell's ISchedulerProvider 方法。

如果您在代码中使用 Observable.Start 而不是 Task.Factory.StartNew,那么您可以在单元测试中使用 TestScheduler 来推送所有计划任务。

例如,您的测试代码可能如下所示:

//Task.Factory.StartNew(() => DoSomething())
//    .ContinueWith(t => DoSomethingElse())
Observable.Start(() => DoSomething(), schedulerProvider.ThreadPool)
          .ToTask()
          .ContinueWith(t => DoSomethingElse())

在你的单元测试中:

// ... test code to execute the code under test

// run the tasks on the ThreadPool scheduler
testSchedulers.ThreadPool.Start();

// assertion code can now run

【讨论】:

  • 这看起来很有趣,当我再次遇到这个问题时,我会记得回到这个答案:D 我做了类似的事情。
  • 这个答案中也有类似的方法:stackoverflow.com/a/17844870/114200,除了它使用任务调度程序并涉及实现你自己的调度程序。我更喜欢上面的方法,但如果你还没有使用 Rx,那么 Task Scheduler 方法可能也会很有趣。
猜你喜欢
  • 1970-01-01
  • 2015-03-06
  • 2011-05-15
  • 1970-01-01
  • 1970-01-01
  • 2019-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多