Once in the realm of asynchronous calls, we have no guarantee of which thread or precisely when an event handler might be called, which makes testing asynchronous events that much more difficult. As a result it makes the test shown below (taken from the previous post unit-testing synchronous events) completely redundant because we rely on the assertion being called after (and only after) the event has fired, which this does not do.
In a synchronous event situation, this test works because the thread which calls
basket.Add is the same thread which executes the lambda expression, therefore it doesn’t return from the
Add method until after local variable
eventFired is set to true. However there are no such guarantees with an asynchronous event.
Because an asynchronous event handler will execute on a different thread to the one which initiated the call (that caused the event to fire) we need some mechanism to stop the original thread from performing any assertions until after the event has fired. There is more than one way to achieve this, but for this example the mechanism comes in the form of an
EventWaitHandle which allows one thread to ‘signal’ another thread that something has happened. In this particular case I’m using the
ManualResetEvent, which as its name suggests remains signalled until it is manually set back to non-signalled.
The example below tests a class called
BackgroundTask (not shown) which performs an operation asynchronously on a separate worker thread and then fires the
TaskComplete event once the task has finished. Because it’s asynchronous, the call to
task.DoCalculationAsync returns almost immediately which is the issue we need to solve.
Rather than the lambda expression setting a boolean value, this time it sets the
ManualResetEvent to signalled via a call to its
Set method. Meanwhile the original thread which called
task.DoCalculationAsync will block on the call to
WaitOne until (a) the wait handle becomes signalled, or (b) the 30 second timeout expires. If the wait handle is signalled within the 30 second time limit, then
eventFired will be set to true but if the timeout expires, it will be set to false and the test will fail.
Timeouts and continuous integration
The previous section raises the obvious issue of what is an acceptable timeout limit in your tests. I included an arbitrary timeout value in the above example just to show the possible options, but if you are testing a background task which might vary in how long it takes to complete, then use a larger value (e.g. 5 minutes) or don’t specify one at all which means it will never timeout. However just bear in mind that this might cause big delays or a complete stand-still on your CI server, so having a ‘fail-safe’ timeout is probably a good idea. The time it takes for tests to complete on your CI environment will vary due to the workload on the server and this should be taken into consideration. A test on your development machine might take twice as long to run on the CI because there might be several build agents running on the same box / virtual.