我的团队在我们的C#项目中广泛使用NUnit单元测试。最近我们开始在.NET 4中使用任务并行库(TPL),它为我们带来了皱纹。
在TPL中,如果任务出现故障(即执行任务时抛出异常),则必须至少检索一次该任务的异常属性。如果不是,当垃圾收集器完成Task对象时,将抛出异常终止进程。
可以通过注册TaskScheduler.UnobservedTaskException的处理程序来检测和防止这种情况。我们已经为我们的一些测试用例重现了未观察到的任务异常错误,但我宁愿有一些方法来修改NUnit运行测试的方式,这样每次测试都会注册一个UnobservedTaskException处理程序,然后在那个测试之后垃圾收集被强制清除任何具有未观察到的异常的任务。我想这会导致测试失败。
其他团队如何解决这个问题?您是如何检测通过的测试用例(即没有任何异常的完整)但是将一个或多个Task对象留下未观察到的异常?
答案 0 :(得分:2)
我是这样做的:
[Test]
public void ObservesTaskException()
{
bool wasUnobservedException = false;
TaskScheduler.UnobservedTaskException +=
(s, args) => wasUnobservedException = true;
CauseATaskToThrowInTheSystemUnderTest();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.That(wasUnobservedException, Is.False);
}
对CauseATaskToThrowInTheSystemUnderTest()的调用是占位符,无论您需要做什么。我建议将代码包装在这样的函数中,因为它有助于确保在GC运行时无法访问抛出异常的Task对象。如果可以访问Task对象,则终结器将不会运行,并且此测试将不会测试任何内容。
显然,在垃圾收集发生之前确保任务已完成也很重要。如何执行此操作(如果可以的话)取决于您的特定代码。也许你的程序流程确保了这种情况。如果没有,并且您的测试可以访问Task对象,则可以执行以下操作:
var continuation = GetTheTaskFromTheSystemUnderTest();
.ContinueWith(t => {});
CauseATaskToThrowInTheSystemUnderTest();
bool isTaskCompleted = continuation.Wait(SomeSuitableTimeout);
您应该将isTaskCompleted添加到断言中。
如果以后代码被破坏,则会出现超时 - 您不希望测试挂起。价值应该很小。如果您发现实际上需要等待很长时间才能完成任务,那么您的测试套件可能会因为频繁使用而太慢。
访问您创建的任务(包括您在其他任务上创建的延续任务)是设计可测试性时要记住的注意事项之一。这样做时,通常的权衡取舍。当我的代码创建任务时,我尝试包含这样的测试 - 这是我的代码的一个重要行为,需要进行测试。
答案 1 :(得分:0)
我会将单元测试保持确定性,然后使用CHESS或Jinx进行功能测试。
这意味着我没有代码依赖单元测试逻辑中的并发流。
至于测试多个异常打包/解包,我会测试它就好像它是一个循环:没有例外,一个例外,两个例外证明了整个事情。