Curiosity is bliss    Archive    Feed    About    Search

Julien Couvreur's programming blog and more

Unittesting asynchronous methods

 

Boomerang catch

How to unittest asynchronous methods in .Net?

If you have encountered this situation, you probably wrote yourself a test harness which understands the asynchronous callback and can wait on either the callback(s) or a timeout.
But like mock objects, writing such helpers objects quickly becomes tedious.


I found a generic solution to avoid most of this repetitive code. The CallbackWaiter class generates your test harness, just like mock frameworks.
You can browse the CallbackWaiter code or get the full project (including unittests and VS project files).

This solution works for any .Net language. The same approach can easily be implemented in Javascript and probably any dynamic language. I'm not sure about Java though (does it support runtime-defined methods?).


Setting a goal:

I started by imagining what my ideal unittest would look like, using the "magical thinking" approach:
[Test]
public void TestAsyncMethod()
{
   MyAsyncService service = new MyAsyncService();

   EventHandler magicHandler =
     new MagicHandler();

   service.CallbackEvent += magicHandler;
   service.DoAsyncWork(3); // Start the asynchronous work. Doesn't block.

   int timeout = 1000;
   MyEventArgs returnValue = magicHandler.Wait(timeout); // Wait for either the event or a timeout

   Assert.AreEqual(3, returnValue.result); // Get the information channeled by the event.
}

The trouble is implementing the "MagicHandler" ;-)

RealProxy, a dead-end:

To implement the MagicHandler, I first looked at the RealProxy class, since it is used by nMock to write generic objects.
RealProxy has the ability to camouflage as a different type (see the RealProxy.GetTransparentProxy method), intercept any calls that it receives and funnel them through a generic IMessage Invoke(IMessage myIMessage) method that you have to implement.

But it turns out that it RealProxy can only emulate MarshalByRef objects. That unfortunately does not work to write a generic delegate, since the Delegate base class is not a MarshalByRef object.

A working solution:

Another approach that looked promising is using the DynamicMethod API, introduced in .Net 2.0, which allows to emit methods at runtime.

After running into the same issues as Rich McColllister and Mike Woodring, including the weird CS0702 error, I ended-up using the same workarounds.

After validating that it is possible to generate dynamic methods and hook them to the generic class, I ironed out some smaller issues: match target signature, record event parameters, support value type parameters (code crashes without a boxing IL instruction), make the class thread safe and support multiple callbacks.

Some tricks learned:

  • When you write code that needs to emit IL, you should write examples of the desired code in C#, compile it and look at the IL generated by the compiler (using ildasm) as an example.
  • When working with DynamicMethods, Haibo Luo's IL visualizer is a useful VS.Net extension which let's you conveniently peek at the IL composing DynamicMethod objects.
  • If you generate bad IL, the CLR may throw an exception (InvalidProgramException) or crash. Both cases are hard to troubleshoot, as you get very little information as to what caused it (I wish the InvalidProgramException would tell you what is invalid about your program...). I would again recommend to compare your IL with some generated by the C# compiler.

Update (2008/12/25):

The Concurrency and Coordination Runtime (CCR) which started as a domain-specific toolkit (for robotics) is being opened up for general purpose. It allows you to deal with asynchrony very cleanly in your code, just like I was trying to achieve with the CallbackWaiter: the code that executes after the asynchronous call is in the same method that started it (no need for large delegates or separate methods).
But CallbackWaiter is obviously very specialized as it only deals with a single async call and has built-in logic for timeout. The CCR allows you to do that and much more.
Learn more with the PDC2008 CCR presentation (video) (the interesting part starts at 9:35).

comments powered by Disqus