August 27, 2008

Unit testing and PInvoking

I'm big on unit testing, I like to test everything and I feel uncomfortable when I'm unable to write tests for some of my code. So even though some parts of your application are hard to test I'm willing to take some extra time to make them testable. In this post I want to show you some tricks I use to unit-test code that uses platform invoked functions.

As an example I'll use one of the invokes from my previous post, SetupDiClassNameFromGuid from the setupapi.dll. If you want to know more about what this does can read that post. Now let's take a look at some code.

   1: public class PInvoking
   2: {
   3:     [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
   4:     public static extern bool SetupDiClassNameFromGuid( 
   5:         ref Guid classGuid, StringBuilder className, UInt32 classNameSize, ref UInt32 requiredSize);
   7:     public void DoSomethingThatNeedsToBeTested() 
   8:     {
   9:         var className = new StringBuilder();
  10:         var classGuid = Guid.Empty;
  11:         var reqSize = (uint)0;
  13:         // Some code that needs to be tested
  14:         // ..
  16:         // PInvode preprocessing
  17:         // ..
  19:         SetupDiClassNameFromGuid(ref classGuid, className, reqSize, ref reqSize);
  21:         // PInvoke postprocessing
  22:         // ..
  24:         // Some more code
  25:         // ..
  26:     }
  27: }

I can see two problems here, testability and reusability. Let's start with making this more reusable although this has nothing to do with unit testing yet.

If you read the previous post you'll know that this call needs some pre-, and post-processing to get something usefull out of it. These technical details are not something you want to bother the consuming function with. DoSomethingThatNeedsToBeTested() just wants to pass a Guid in and get a string back. And in the real world there are probably more consumers of this imported function so you'll want to get rid of this duplicated code and hide it somewhere you can reuse it with a single call.

You can split out the preprocessing and postprocessing into a separate function but it would be better to go a bit further and just put everything in a separate class. I even made the DllImport private so the consumers are forced to use the code with all the wrapping code. I used the wrapper code from my previous post here to show how fast things get complex. In real life you'll probably want to add some errorhandling too.

   1: public class SetupApi
   2: {
   3:     [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
   4:     private static extern bool _SetupDiClassNameFromGuid(
   5:         ref Guid classGuid, StringBuilder className, UInt32 classNameSize, ref UInt32 requiredSize);
   7:     public static bool SetupDiClassNameFromGuid(Guid classGuid, ref string className)
   8:     {
   9:         // 50 is a Sensible default value, if we're lucky it's enough
  10:         uint reqSize = 50;
  11:         bool returnValue;
  12:         StringBuilder classNameBuilder = new StringBuilder((int)reqSize);
  14:         // First try.
  15:         returnValue = _SetupDiClassNameFromGuid(
  16:             ref classGuid, classNameBuilder, (uint)classNameBuilder.Capacity, ref reqSize);
  18:         if ((uint)classNameBuilder.Capacity != reqSize)
  19:         {
  20:             // call again with the right size stringbuilder
  21:             classNameBuilder.Capacity = (int)reqSize;
  22:             returnValue = _SetupDiClassNameFromGuid(
  23:                 ref classGuid, classNameBuilder, reqSize, ref reqSize);
  24:         }
  26:         className = classNameBuilder.ToString();
  27:         return returnValue;
  28:     }
  29: }

Things are a bit more reusable now. But they're still not testable. And we've created a second problem. We don't just want to test the consuming code but we want to test the wrapper around _SetupDiClassNameFromGuid too.

The first problem is easy to solve. Just remove the static keyword from line 7. Then you can use dependency injection to inject this class into the consuming classes or functions and you can mock up everything. I'll show an example in my next post.

The second problem is a bit harder. I have a couple of solutions for this too but the are not very straight-forward and involve some extra code. I'll explain these later too.

No comments:

Post a Comment