How Can I Make This More Testable?

I’ve been going through a refactoring exercise recently with my SSIS data source component for OData (or WCF data services to be more specific).  The primary goals of the refactoring were:

  • Move as much logic as possible out of the SSIS adapter code, which I look at as the equivalent of presentation code, and move it into my domain.
  • Simplify the domain (ideally improving maintainability and future extensibility) by doing a better job of focusing each class around a single responsibility (or getting closer than I was)
  • Improve testability – both unit level and end-to-end

I’m pretty happy with how the code is turning out (though I would welcome your general thoughts and feedback) with one major exception. 

The crux of the problem is the SSIS object model.  For a bit more context, here’s the part of my domain that makes an OData response (typically an Atom feed) available to SSIS.

ResultOM

The problem is in the SetPipelineValue method on the Result class.  The purpose of the Result class is to make a row of data available to an SSIS pipeline buffer.  Because SSIS has its own data types, and sets values on the buffer through its own get and set functions, I created this method to encapsulate the logic necessary to get from Atom result to .NET type to SSIS type to getting set on a buffer.  The code looks like the following:

public class Result : IResult
{
    private static IDictionary<DataType, Action<PipelineBuffer, int, object>> typeAssignmentFcnMap = new Dictionary<DataType, Action<PipelineBuffer, int, object>> {
        { DataType.DT_NTEXT, (buffer, index, val) => buffer.AddBlobData(index, Encoding.Unicode.GetBytes(val.ToString())) }, 
        { DataType.DT_WSTR, (buffer, index, val) => buffer.SetString(index, (string)val) }, 
        { DataType.DT_R8, (buffer, index, val) => buffer.SetDouble(index, (double)val) }, 
        { DataType.DT_I4, (buffer, index, val) => buffer.SetInt32(index, (Int32)val) },
        { DataType.DT_BOOL, (buffer, index, val) => buffer.SetBoolean(index, (bool) val) },
        { DataType.DT_GUID, (buffer, index, val) => buffer.SetGuid(index,(Guid)val) },
        { DataType.DT_DBTIMESTAMP, (buffer, index, val) => buffer.SetDateTime(index, (DateTime)val) },
        { DataType.DT_I2, (buffer, index, val) => buffer.SetInt16(index, (Int16)val)},
        { DataType.DT_I8, (buffer, index, val) => buffer.SetInt64(index, (Int64)val)},
        { DataType.DT_DECIMAL, (buffer, index, val) => buffer.SetDecimal(index, (decimal)val)}
    };

    public void SetPipelineValue(string propertyName, PipelineBuffer ssisBuffer, int ssisPositionInBuffer, DataType ssisDataType) {
        var index = GetPropertyIndex(propertyName);

        var fieldValue = _values[index];
        typeAssignmentFcnMap[ssisDataType](ssisBuffer, ssisPositionInBuffer, fieldValue);
    }
}

The problem comes when I’m trying to write tests to verify that the buffer was set with the correct values of the correct SSIS data type.  As you can see, the buffer is represented by the PipelineBuffer class – this is a concrete class with no interface or virtual members to make it mockable.  As such, I can’t send in a mock buffer and then verify that the appropriate values are set (or at the very least, verify that the appropriate methods were called).

At this point, the best solution that I’ve got is to wrap the PipelineBuffer in a class (and the the SetXx methods) and then mock that class in testing.  I’m ok with the additional code here because it will enable me to run end to end tests (at the moment, I can’t inspect the final values to be set on the buffer) – but wanted to see whether anybody had a different approach for this problem.

About Howard Dierking

I like technology...a lot...
This entry was posted in ADO.NET Data Services, BDD, SSIS. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Srihari Kothapalli

    Thank you Howard. This was useful.

  • NoMe

    Build Up an (Testing)Aspect (e.g. PostSharp) and check the Pre/PostConditions there.

  • Anonymous

    @Chris – I did consider rolling in TypeMock, as I’ve used it succesfully in the past for dealing with legacy APIs like this. In this case, however, the amount of work involved for introducing the abstraction was low enough to where it was worth it to me (follow the code links for the details)

  • Chris Tavares

    I’m going to be a little heretical. Have you considered, rather than introducing an abstraction just so you can mock stuff out, just pulling out a bigger gun mocking-wise? TypeMock or Moles would make short work of it without having to add another abstraction just for testability.

    If it was being built from scratch, I’d absolutely fix the API so you didn’t have this problem. But it isn’t, and you can’t, so this strikes me as a good time to take advantage of the bigger tools.

  • Anonymous

    @Ben – the abstraction wasn’t originally apparent to me because instead of having a result class, my query was simply handing back arrays and the SSIS buffer management logic was in my SSIS component code (which has a bunch of dependencies on the SSIS runtime ctx and is not very testable). When I did the refactoring work, I pulled a lot of that code from the component into my domain, and that’s where new abstractions like Result emerged. Test-driving Result led the buffer abstraction to emerge for the reasons I talked about in the post.

  • http://ben.biddington.myopenid.com/ Ben Biddington

    To me this implies you have a missing abstraction. The “mock roles not objects” crowd would tell you that as you were test-driving your Result class you would have introduced the Buffer abstraction first — before creating the implementation in terms of PipelineBuffer.

  • Anonymous

    Hey all – I moved ahead with the lightweight wrapper and here’s a high level of the changes I made:

    1) created an interface for the buffer behaviors that my domain actually cares about (https://bitbucket.org/howarddierking/ssis-wcf-dataservices/src/eaefb761149f/SSIS.DataServicesComponents%20Solution/SSIS.DataServicesComponents.Domain/IBuffer.cs)

    2) created a SSIS buffer implementation which forwards calls to the actual SSIS buffer (https://bitbucket.org/howarddierking/ssis-wcf-dataservices/src/eaefb761149f/SSIS.DataServicesComponents%20Solution/SSIS.DataServicesComponents/SsisBufferWrapper.cs)

    3) created a fake buffer for my tests which just stores the values set by the map delegates into a List which can then be inspected by the test. Should I find the need later, I can add spying functionality for things like recording the name of the function called, etc. (https://bitbucket.org/howarddierking/ssis-wcf-dataservices/src/eaefb761149f/SSIS.DataServicesComponents%20Solution/SSIS.DataServicesComponents.Test/FakePipelineBuffer.cs)

    4) updated my tests to use the fake buffer (https://bitbucket.org/howarddierking/ssis-wcf-dataservices/src/eaefb761149f/SSIS.DataServicesComponents%20Solution/SSIS.DataServicesComponents.Test/Integration/DailyTreasuryYieldCurveRateDataSpecs.cs) and my SSIS component code to use my “real” buffer (https://bitbucket.org/howarddierking/ssis-wcf-dataservices/src/eaefb761149f/SSIS.DataServicesComponents%20Solution/SSIS.DataServicesComponents/DataServicesDataSource.cs – line 219)

    thoughts?

  • http://blogs.msdn.com/gblock Glenn Block

    I do the same things in terms of wrappers. I recently used a similar approach to stub out WebOperationContext so that my service didn’t depend on it for an Owin spike I was doing. I created a set of factories here: http://codepaste.net/a895oz which then were used in the implementation here: http://codepaste.net/iczw5u

    When you are up against the wall, simple patterns i.e. adapters / decorators are your friend!

    As a side note fortunately we are removing alot of those static dependencies going forward in our new stuff (as you know)

  • Ray Oneill

    Making a simple mockable wrapper is pretty much what I’d do. Pretty common for when I have to deal with static/sealed Microsoft stuff that is prohibitive to mocking, i.e. Sharepoint or lots of System.IO stuff.

  • http://twitter.com/jediwhale Mike Stockdale

    SetPipelineValue with 4 parameters is a smell – seems like there’s another class lurking in here. Is PipelineBuffer your class or a library class you’re using? If it’s a library class then wrap with an interface you can mock and also try to do an integration test to make sure your wrapper is using it correctly.

  • http://twitter.com/giorgiosironi Giorgio Sironi

    I think the root cause here is that setPipelineValue depends on a concrete class, instead of on an abstraction (an interface containing PipelineBuffer’s setters, but not other methods which are not called here). Since I guess you can’t modify PipelineBuffer to extract an interface, wrapping it is the right choice.

  • http://twitter.com/anshulbajpai Anshul Bajpai

    I guess what atmost you can test here is whether your delegates are getting called here. But for that you will have to inject your Dictionary from outside and you will have to create an abstraction over your delegates on which you will set expectations by mocking them.
    If you also need to verify the actual method calls on buffer, you will have to create an abstraction on top of it(like the way you said).
    But I feel that shouldn’t be tested here because the dictionary itself should’nt be part of this class.

  • http://twitter.com/agileHans Hans Løken

    I think your approach of wrapping the un-mockable concrete class with your own mockable class is a good idea. I have done the same in the past with DirectoryInfo and similar to mock out the file-system.

    However, in this case, if the implementation of the PipelineBuffer has methods/properties that lets you directly control/check it’s content I would consider not mocking it at all. You would still have reasonably good control of input/output of the method under test test and you wouldn’t be adding another level of indirection just to enable testing (which can make your design more complex in the long run). You would, however, sacrifice complete isolation, making it less of a unit-test according to most definitions.

    Difficult to say which approach I’d use without knowing more about your project.