September 30, 2008

Unit testing and PInvoking pt.3

In the previous parts of this short series (that took a long time to finish) I looked at testing code that depends on pinvoke calls. I did this by wrapping the PInvoked functions in a class and then injecting that class into the depending code. I used the wrapper class to put some checks around the platform invokes to smooth some rough edges of the calls to unmanaged land. This creates a nice reusable interface around these calls but now we have the same problem we started with, testability. In this post I'll look at adding a final layer of indirection to be able to test this code.

You can find the previous posts in this series here.

First I'll start with a warning. Because C# forces us to import Dll functions as static functions it's hard to mock them without writing extra code that doesn't really add any functionality. But in most cases I think the added testability is worth the extra verbosity. But it's not the nicest code ever written.

Now lets look back at where we were.

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

The SetupApi is mockable with rhino mocks so that we can test consuming code. Now let's see how we can test the SetupDiClassNameFromGuid function.


If you're using TypeMock you can stop reading now. TypeMock is a very powerfull mocking framework that can easilly mock the static DllImport. I'm using RhinoMocks. This means I'll have to do some extra work.


First the ugly stuff I warned you about at the start of the article. We'll have to put an adapter around the DllImported function to make it non-static.



   1: private bool SetupDiClassNameFromGuid(
   2:     ref Guid classGuid, StringBuilder className, UInt32 classNameSize, out UInt32 requiredSize)
   3: {
   4:     return _SetupDiClassNameFromGuid(ref classGuid, className, classNameSize, out requiredSize);
   5: }

Now we could just make this function public and use a partial mock to insert our test-code but this would open up the raw DllImport to the outside world. This is not something I'm a big fan of. And we have three different functions called something like SetupDiClassNameFromGuid. So things are getting a bit confusing.


This is the way I usually clean up this mess. With an inner class for the DllImport and some constructor magic to make things injectable.



   1: public class SetupApi
   2: {
   3:     public class DllImport
   4:     {
   5:         [DllImport("setupapi.dll",
   6:             EntryPoint = "SetupDiClassNameFromGuid",
   7:             CharSet = CharSet.Auto,
   8:             SetLastError = true)]
   9:         private static extern bool _SetupDiClassNameFromGuid(
  10:             ref Guid classGuid, StringBuilder className, UInt32 classNameSize, out UInt32 requiredSize);
  11:  
  12:         public virtual bool SetupDiClassNameFromGuid(
  13:             ref Guid classGuid, StringBuilder className, UInt32 classNameSize, out UInt32 requiredSize)
  14:         {
  15:             return _SetupDiClassNameFromGuid(ref classGuid, className, classNameSize, out requiredSize);
  16:         }
  17:     }
  18:  
  19:     private DllImport _import;
  20:  
  21:     public SetupApi() : this(new DllImport()) { }
  22:  
  23:     public SetupApi(DllImport import)
  24:     {
  25:         _import = import;
  26:     }
  27:  
  28:     public virtual bool SetupDiClassNameFromGuid(Guid classGuid, out string className)
  29:     {
  30:         // bla...
  31:  
  32:         returnValue = _import.SetupDiClassNameFromGuid(
  33:             ref classGuid, classNameBuilder, (uint)classNameBuilder.Capacity, out reqSize);
  34:  
  35:         // bla...
  36:     }
  37: }

As you can see I added a parameterless constuctor for use in normal code. Normally I don't do this but in this case we're pretty sure the only two uses of this class will be with the inner class in day-to-day life and with a mocked inner class for tests. So this will clean up calling code a bit.


Here is what a test might look like, here we're testing if our wrapper calls the pinvoked call with an initial buffer of 50.



   1: [Test]
   2: public void ShouldTryCallWithBufferLength50()
   3: {
   4:     var mockDll = MockRepository.GenerateMock<SetupApi.DllImport>();
   5:     var api = new SetupApi(mockDll);
   6:  
   7:     string className;
   8:     var classGuid = Guid.NewGuid();
   9:  
  10:     mockDll.Expect(
  11:         x => x.SetupDiClassNameFromGuid(
  12:             ref Arg<Guid>.Ref(Is.Anything(), classGuid).Dummy,
  13:             Arg<StringBuilder>.Is.Anything, 
  14:             Arg<uint>.Is.Equal((uint)50), 
  15:             out Arg<uint>.Out(51).Dummy))
  16:         .Return(true);
  17:  
  18:     api.SetupDiClassNameFromGuid(classGuid, out className);
  19:  
  20:     mockDll.VerifyAllExpectations();
  21: }

And another one to see if there's a retry when the buffer is bigger than 50



   1: [Test]
   2: public void ShouldRetryIfStringTooLarge()
   3: {
   4:     var mockDll = MockRepository.GenerateMock<SetupApi.DllImport>();
   5:     var api = new SetupApi(mockDll);
   6:  
   7:     string className;
   8:     var classGuid = Guid.NewGuid();
   9:  
  10:     mockDll.Stub(
  11:         x => x.SetupDiClassNameFromGuid(
  12:              ref Arg<Guid>.Ref(Is.Anything(), classGuid).Dummy,
  13:              Arg<StringBuilder>.Is.Anything,
  14:              Arg<uint>.Is.Equal((uint)50),
  15:              out Arg<uint>.Out(51).Dummy))
  16:         .Return(true);
  17:  
  18:     api.SetupDiClassNameFromGuid(classGuid, out className);
  19:  
  20:     mockDll.AssertWasCalled(
  21:         x => x.SetupDiClassNameFromGuid(
  22:             ref Arg<Guid>.Ref(Is.Anything(), classGuid).Dummy,
  23:             Arg<StringBuilder>.Is.Anything,
  24:             Arg<uint>.Is.Equal((uint)50),
  25:             out Arg<uint>.Out(51).Dummy));
  26: }

By the way. I really like the rhino mocks AAA model. Using it together with the Arg object like here makes it still rather readable to mock and assert calls to rather complex functions with ref and out parameters mixed together. Although I did find one small problem. If you remove the cast to uint on line 24 the code still compiles but the test fails. Int 50 should be equal to uint 50 but this doesn't work here. But this could be a problem with the rhino mocks 3.5 beta I'm using.

No comments:

Post a Comment