Isolator trick – Unit Testing ThreadPool.QueueUserWorkItem()

Unit testing asynchronous stuff is tough. This is something I had to live with for the last few days working on one of our products. This is compounded by having to unit test jobs that go through ThreadPool.QueueUserWorkItem() – because it’s using the ThreadPool it’s hard to synchronize your test. If you just do this:

   1: [Test]
   2: public void TestAsyncStuff()
   3: {
   4:    bool setInBackground = false;
   5:  
   6:    ThreadPool.QueueUserWorkItem(o => {setInBackground = true;});
   7:  
   8:    Assert.IsTrue(setInBackground);
   9: }

Your test will almost always succeed. But then again it just might fail – there’s a race condition between the assertion and the background thread on setInBackground. This is of course a hugely simplified case – usually the call to QueueUserWorkItem() is hidden somewhere in the code under test where it does time consuming calculations and not just setting boolean values. These race conditions may cause nastier results than a simple dirty bool, and may be quite hard to debug.

Now, it’s our responsibility to synchronize our tests – always make sure everything in the code under test finished running before asserting against it. But how does one synchronize the ThreadPool? We all know how to synchronize a Thread, right? Just .Join() on it and you’re good. How about we turn the mighty ThreadPool into a puny Thread using Isolator?

First, we need to refactor the call to ThreadPool.QueueUserWorkItem() to a method we can fake – unfortunately, we can’t fake it directly because it’s in mscorlib. Let’s say we did it like this:

   1: public class ThreadingWrapper

   2: {

   3:    public void QueueAsyncJob(Action<object> job)

   4:    {

   5:       var callback = new WaitCallback(job);

   6:  

   7:       ThreadPool.QueueUserWorkItem(callback);

   8:    }

   9: }


Now, using DoInstead() we turn the call to QueueAsyncJob to a call to a Thread. This way we maintain the asynchronous nature of the code under test, and maintain the capability to synchronize our test without jumping through hoops. For instance:

   1: [Test, Isolated]

   2: public void TestAsynchStuff()

   3: {

   4:    bool setInBackground = false;

   5:    Thread backgroundThread = null;

   6:  

   7:    Isolate.WhenCalled(() => ThreadingWrapper.QueueAsyncJob(null))

   8:       .DoInstead(context => 

   9:                          {

  10:                              var job = context.Parameters[0] as Action<object>;

  11:                              backgroundThread = new Thread(() => job(null));

  12:                              backgroundThread.Start();

  13:                           });

  14:  

  15:    ThreadingWrapper.QueueAsyncJob(o => {setInBackground = true;});

  16:  

  17:    backgroundThread.Join();

  18:  

  19:    Assert.IsTrue(setInBackground); // if we reached this we can be sure the asynchronous code finished running

  20: }


Mission accomplished! Lets do a quick recap – we need to test code that uses ThreadPool.QueueUserWorkIteam(). This is not fun because we can’t easily wait for all worker threads to finish, and it’s hard to synchronize our tests that way. Solution – we switch from using ThreadPool to using a regular Thread! How? extract the call to ThreadPool into a method, and when that method is called, replace its implementation (using the wonderfully flexible DoInstead()) with a simple thread creation and invocation. Before you assert on the test’s result, just call Thread.Join() and you’re good!

TOP