单元测试代码,使用Task.Factory.StartNew()。ContinueWith()

时间:2012-11-09 13:19:56

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

所以我有一些代码

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 ......但这似乎是一种真正的hacky方式。

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

5 个答案:

答案 0 :(得分:2)

请考虑以下事项:

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();

答案 1 :(得分:2)

我认为没有一种简单实用的方法可以做到这一点。我刚才遇到同样的问题而Thread.Sleep(X)是解决这个问题的最简单(如果不是优雅)方式。

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

public interface ITaskWrapper
{
    void TaskMethod();
}

您的具体实施:

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

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

答案 2 :(得分:1)

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

E.g。

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

sut.DoStuff();

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

答案 3 :(得分:0)

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

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

Thread.Sleep(100);

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

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

答案 4 :(得分:0)

在使用Reactive Extensions的测试代码中处理异步进程时,一种方法是使用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