Saturday, April 18, 2009

Mocking & Stubbing BizTalk Map Extension Objects

If you're a red/green TDD developer, BizTalk can be frustrating. There are some cool frameworks available but nothing beats the ability to test a unit of work in isolation without having to plumb-up peripheral services. BT2K9 goes some way to appeasing the masses but testing maps in isolation is not afforded out of the box.

Why would you want to isolate BizTalk maps from extension objects? Answer: because the time spent in setting up and tearing down external resources (eg) databases or config stores, to ensure your tests pass every time can be prohibitive and test results may vary between environments. This can be read as 'a waste of time' or 'burning cash' for comparatively little reward.

So, we have to work within the framework of the BizTalk object model. No problem, let's do it...

The trick in creating unit testable maps with mock extension objects is in leveraging the TransformBase object model. In particular the XsltArgumentListContent property. Parameters and objects can be passed into transformations from .Net. The meat in mocking extension objects stems from this affordance.

To explain further, the construct reference below will be familiar:

// Execute the map
mapInstance.Transform.Transform(xpath, mapInstance.TransformArgs, mapResultWriter);

What the test (or framework) needs to do is new up an XsltArgumentList object that hosts your mocks and stubs and use that instance in place of the instance from TransformBase.TransformArgs.

Apart from the fact that you need to build an XsltArgumentList object you need a facade to host your mock object because the dynamic mock cannot, obviously, be late bound by the transform at runtime. Note that a concrete implementation (ie) the facade, is required because the transform tries to resolve the class name of the extension object at runtime. As such, the facade must relay the request to this mock verbatim. Easy. An example follows:

public class FacadeDateTimeHelper: IDateTimeHelper
{
    
private IDateTimeHelper _mock;

    
public FacadeDateTimeHelper(IDateTimeHelper mock)
    {
        _mock = mock;
    }

    
/// <summary>
    /// Delegate the call to the mock verbatim
    /// </summary>
    public string Format(string target, string inputFormat, string outputFormat)
    {
        
return _mock.Format(target, inputFormat, outputFormat);
    }
}


Something to note is that the facade in the example above implements an interface. You may think this implies that the real extension object is not your traditional static BizTalk helper. You'd be right! But this doesn't mean that static method call expectations can't be tested in exactly the same manner. Cool.

Now that the facade is in place we need to consider whether we'll ALWAYS want to mock ALL the extension objects in the argument list. If not, there needs to be some smarts built into the arg list builder to preserve the other extension objects but replace the required objects with our own mock facade(s). The extension objects are referenced using name-value pairs where the name is the namespace in the map's raw Xslt and the value is the related extension object instance. This namespace commonly takes the format http://schemas.microsoft.com/BizTalk/2003/ScriptNSn where n is the zero-based index of the extension object.

In the code example below the user of the Replace method must construct a Dictionary specifying the namespace of the extension object to replace in the existing argument list and the mock facade instance to replace it with. The realXtensions parameter below is constructed by deserialising the TransformBase.XsltArgumentListContent property. Note that you can use xsd.exe to build an ExtensionObjects class from the raw Xml in the XsltArgumentListContent property. The ExtensionObject Xml can also be obtained by 'validating' the BizTalk map at design time.

/// <summary>
///
Substitute existing arguments with object references provided using the namespace as a matching key
/// </summary>
///
<param name="realXtensions">Complete extension object collection to be fully or partially replaced</param>
///
<param name="replacementArgList">Substitution objects</param>
///
<returns>A recompiled extenstion object collection to be used when mapping</returns>
public static XsltArgumentList Replace(ExtensionObjects realXtensions, Dictionary<string, object> replacementArgList)
{
    
XsltArgumentList newArgList = new XsltArgumentList();
    
foreach (ExtensionObjectsExtensionObject xtension in realXtensions.Items)
    {
        
string ns = xtension.Namespace;
        
if (replacementArgList.ContainsKey(ns))
        {
            newArgList.AddExtensionObject(ns, replacementArgList[ns]);
        }
        
else
        {
            
string assemblyName = xtension.AssemblyName;
            
string className = xtension.ClassName;

            
ObjectHandle handle = Activator.CreateInstance(assemblyName, className);
            
object xtensionObject = handle.Unwrap();
            newArgList.AddExtensionObject(ns, xtensionObject);
        }
    }
    
return newArgList;
}


Your BizTalk map test framework will then leverage the new XsltArgumentList object like this:

/// <summary>
///
Execute the mapping procedure and determine if the output is as expected
/// </summary>
///
<remarks>
///
If tester has provided an Xslt extension object collection then replace the map's
/// default instances using the script namespace to resolve
/// </remarks>
///
<typeparam name="T">BizTalk map type</typeparam>
///
<param name="input">Map source</param>
///
<param name="expectedOutput">Expected map output</param>
///
<returns>Success result with actual output on failure</returns>
public MapResult Map<T>(Stream input, Stream expectedOutput) where T : TransformBase
{
    
XPathDocument xpath = new XPathDocument(input);
    
TransformBase mapInstance = Activator.CreateInstance<T>();
    
XsltArgumentList transformArgs;

    
// Substitute the external assemblies?
    if (_substituteXtensions.Count == 0)
    {
        transformArgs = mapInstance.TransformArgs;
    }
    
else
    {
        
string args = mapInstance.XsltArgumentListContent;
        
ExtensionObjects xtensions = XmlHelper.Deserialize<ExtensionObjects>(args);
        transformArgs =
XsltArgListBuilder.Replace(xtensions, _substituteXtensions);
    }

    
using (MemoryStream mapResult = new MemoryStream())
    {
        
using (XmlWriter mapResultWriter = XmlHelper.FormattedXmlWriter(mapResult))
        {
            
// Execute the map
            mapInstance.Transform.Transform(xpath, transformArgs, mapResultWriter);
            mapResult.Position = 0;
            
return _comparer.Execute(mapResult, expectedOutput);
        }
    }
}


Your unit test method will use the test framework similar to this:

/// <summary>
///
Mock the helper class to test expectations
/// </summary>
[TestMethod]
public void CanMapInput_To_Output_WithExtensionObjects()
{
    
// Set up the test resources
    string sourceFile = "Input_To_Output_WithExtensionObjects.Source.xml";
    
string expectedFile = "Input_To_Output_WithExtensionObjects.Expected.xml";
    
Stream source = ResourceHelper.GetEmbeddedResource(_assembly, sourceFile);
    
Stream expected = ResourceHelper.GetEmbeddedResource(_assembly, expectedFile);

    
// Set the expectations
    Mockery mockFx = new Mockery();

    
IDateTimeHelper dtMock = mockFx.NewMock<IDateTimeHelper>();
    
FacadeDateTimeHelper dtFacade = new FacadeDateTimeHelper(dtMock);
    
Expect.Once.On(dtMock).Method("Format")
        .With(
"31/03/2009 2:1:9", "dd/MM/yyyy H:m:s", "yyyy-MM-dd")
        .Will(
Return.Value("2009-03-31"));
    
Expect.Once.On(dtMock).Method("Format")
        .With(
"31/03/2009 2:1:9", "dd/MM/yyyy H:m:s", "yyyy-MM-ddTHH:mm:ss.fff")
        .Will(
Return.Value("2009-03-31T02:01:09.000"));

    
// Inject the mocks
    Dictionary<string, object> mockHelpers = new Dictionary<string, object>();
    mockHelpers.Add(
String.Format(MapTester.SCRIPT_OBJ_NS_FORMAT, 0), dtFacade);
    
MapTester tester = new MapTester(mockHelpers);

    
// Execute the test
    MapResult result = tester.Map<Input_To_Output_WithExtensionObjects>(source, expected);

    
// Verify expectations
    mockFx.VerifyAllExpectationsHaveBeenMet();
    
MapTester.AssertSuccess(result, sourceFile, expectedFile);
}


If we walk the test method above we can see that the sample input and expected output documents are retrieved from the embedded resource cache in the test fixture assembly. The input is well-known and the test reflects it's profile. The mocking framework (NMock 2.0 in this example) is then loaded and expectations are set against the dynamic mock of the IDateTimeHelper interface which has been injected into the facade. The test framework is then called to substitute the extension objects and execute the map. The mocking framework is then queried to ensure all expectations were met and the actual & expected output equality assertion is made.

No comments:

Post a Comment