When developing service oriented applications, developers
must master asynchronous programming. Fortunately, .NET shields us from many of
the intricacies of the asynchronous programming model. In a Windows Forms
application you can use AsyncCallbacks out of the box, because the calling
application is likely to be running when an asynchronous method returns, no
matter whether the operation is long running or not. Unit tests are different.
Tests are often small, and the tests are reinitialized between every single
test within a fixture. As a consequence of this, a test is likely to complete
in good time before the callback occurs. The below example doesn’t test the
callback, because the callback will occur after the TestCalculator method
completes.
[TestFixture]
public class ATestThatDoesntWork
{
[Test]
public void TestCalculator()
{
Calculator
calc = new Calculator();
SquareDelegate
asyncMethodDelegate = new SquareDelegate(calc.Square);
int
valueToSquare = 4;
IAsyncResult
result = asyncMethodDelegate.BeginInvoke(valueToSquare, new AsyncCallback(SquareCallback),
asyncMethodDelegate);
}
public delegate int SquareDelegate(int
value);
public static void
SquareCallback(IAsyncResult result)
{
SquareDelegate
asyncMethodDelegate = result.AsyncState as SquareDelegate;
int
squaredValue = asyncMethodDelegate.EndInvoke(result);
Assert.AreEqual(16,
squaredValue);
}
}
An easy solution to this problem is to insert a Thread.Sleep
at the end of the TestCalculator method. A problem with this approach is choosing
how long the thread should sleep. If you make the period too short, you run the
risk that the asynchronous method takes longer to execute than the thread’s
nap, and you end with a test that doesn’t really exercise the code. If you make
the period too long, you’ll end up with a protracted test suite, and yet you’ll
have no guarantee that the callbacks will get tested. Just imagine the impact
of a Thread.Sleep(10000) in every single test within a large enterprise
project.
A much more elegant and by far less time-consuming approach
is to leverage a WaitHandle to wait for the asynchronous call to complete. The
example below (C# 2.0) shows how the previous example can be improved.
[TestFixture]
public class CS20CalculatorTest
{
[Test]
public void TestCalculator()
{
Calculator
calc = new Calculator();
SquareDelegate
asyncMethodDelegate = new SquareDelegate(calc.Square);
int
valueToSquare = 4;
ManualResetEvent
waitHandle = new ManualResetEvent(false);
IAsyncResult
asyncResult = asyncMethodDelegate.BeginInvoke(valueToSquare, new AsyncCallback(
delegate(IAsyncResult result)
{
SquareDelegate
localAsyncMethodDelegate = (SquareDelegate)result.AsyncState;
int
squaredValue = localAsyncMethodDelegate.EndInvoke(result);
Assert.AreEqual(16,
squaredValue);
waitHandle.Set();
}), asyncMethodDelegate);
waitHandle.WaitOne();
}
public delegate int SquareDelegate(int
value);
}
This example uses a ManualResetEvent, which is a class that inherits
from WaitHandle, to block the thread executing the TestCalculator method until the
handle is signaled. I’ve used an anonymous method, which is one of the many new
features in C# 2.0, to implement the AsyncCallback delegate. This makes keeps
all the test code within the test method, as opposed to the previous example
where it is spilt into two methods. The anonymous method has access to the TestCalculator’s
local variables. This enables me to declare and instantiate the
ManualResetEvent within the test, and signal the handle from within the
anonymous method. The ManualResetEvent.Set method gives the signal, and the ManualResetEvent.WaitOne
method blocks the thread until the handle has received one signal. This enables
the test to proceed as soon as the callback is completed, but not before. Although
there are huge benefits with this approach, it has one weakness. If for some
reason the asynchronous method never calls back, you run the risk of the test waiting
forever. Eddie Garmon has developed a
NUnit extension that enables you to set a timeout for tests. This is an
excellent tool to guard against infinite tests, just remember to make the
timeout period long enough for the asynchronous call to complete.