How to detect UnobservedTaskException errors in nunit test suites
My team makes extensive use of NUnit unit tests in our C# project. Recently we have started using the Task Parallel Library (TPL) in .NET 4, which has introduced开发者_JAVA百科 a wrinkle for us.
In the TPL, if a Task is faulted (that is, an exception was thrown while executing the task), that task's Exception property must be retrieved at least once. If it is not, when the Task object is finalized by the garbage collector an exception will be thrown which terminates the process.
It is possible to detect and prevent this by registering a handler for the TaskScheduler.UnobservedTaskException. We have done that for a few of our test cases to reproduce unobserved task exception bugs, but I would rather have some way to modify the way NUnit runs the tests so that for each test, an UnobservedTaskException handler is registered, then after that test a garbage collection is forced to flush out any tasks with unobserved exceptions. I would then like this to cause the test to fail.
How are other teams solving this problem? How are you detecting test cases that pass (that is, complete without any exceptions) but leave one or more Task objects with unobserved exceptions?
Here is how I do it:
[Test]
public void ObservesTaskException()
{
bool wasUnobservedException = false;
TaskScheduler.UnobservedTaskException +=
(s, args) => wasUnobservedException = true;
CauseATaskToThrowInTheSystemUnderTest();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.That(wasUnobservedException, Is.False);
}
The call to CauseATaskToThrowInTheSystemUnderTest() is a placeholder for whatever you need to do. I recommend wrapping your code in a function like this because it aids in making sure the Task object that throws the exception is unreachable when the GC runs. If the Task object is reachable, the finalizer won't run and this test will not test anything.
Obviously, it is also important to make sure the task has completed before garbage collection occurs. How you do this (if you even can) is dependent on your particular code. Perhaps the flow of your program ensures this is the case. If not, and if your test can access to the Task object, you can do the following:
var continuation = GetTheTaskFromTheSystemUnderTest();
.ContinueWith(t => {});
CauseATaskToThrowInTheSystemUnderTest();
bool isTaskCompleted = continuation.Wait(SomeSuitableTimeout);
You should add isTaskCompleted to your assertion.
The timeout is there just in case the code is broken in the future--you don't want your test to hang. The value should be very small. If you find you actually need to wait very long for your task, your test suite will likely be too slow for frequent use.
Having access to the tasks you create (including continuation tasks you create on other tasks) is one of the considerations to keep in mind while designing for testability. When doing so, the usual tradeoffs apply. I try to include tests like this when my code creates tasks--this is an important behavior of my code and it needs to be tested.
I would keep unit tests deterministic, then do functional testing with CHESS or Jinx.
This would mean that I have no code that relies on concurrent flows in the unit testing logic.
As for testing multiple exceptions packaging/unpackaging, i would test it as if it was a loop: no exceptions, one exception, and two exceptions prove the whole thing.
精彩评论