Unit-Testing Custom Inline Functoids with the CodeDOM

As far as extending the BizTalk Mapper, custom inline functoids
are probably one of the easiest ways. This particular types of functoids are what I called design-time functoids in the sense
that they are only required to be made available to Visual Studio. Once dropped on the design surface, they are responsible for emitting
code that gets embedded inside the assembly once compiled.

In order to do that, custom inline functoids are C# classes that typically implement a GetInlineScriptBuffer
method to return a chunk of code during compilation.

However, as the MSDN documentation referred to above states, custom inline functoids can be challenging to test. Indeed, the documentation suggests to either carefully inspect the resulting XSLT code or verify the input and output of a map that makes use of the particular functoid you want to test.

The TDD-way of Developing Custom Inline Functoids

As a fan of the Test-Driven Development process, I wanted a cleaner, more robust way to test the chunk of code that gets emitted as part of the GetInlineScriptBuffer method.

Ideally, it should be possible to test the very same code that the GetInlineScriptBuffer method returns without risking modifying the code by cutting and pasting and escaping quotes and adding a bunch of StringBuilder.Append wrapping calls!

Thanks to the power of the .net framework CodeDOM I thought one could find a way to test custom inline functoids in a way that is more integrated with my build process. Here is what I came up with:

A Simple Unit-Testing Helper Class

using System.CodeDom.Compiler;
using System.Reflection;
using Microsoft.CSharp;

class TestHelper
{
    public static string GetInlineScriptBuffer(string typeName, int numParams, int functionNumber)
    {
        object[] args = new object[] { Microsoft.BizTalk.BaseFunctoids.ScriptType.CSharp, numParams, functionNumber };

        Type type = Type.GetType(typeName);
        object target = Activator.CreateInstance(type);

        return (string) type.InvokeMember(
            "GetInlineScriptBuffer"
            , BindingFlags.Instance
            | BindingFlags.NonPublic
            | BindingFlags.InvokeMethod
            , null
            , target
            , args);
    }

    public static string GenerateSourceCode(string source)
    {
        StringBuilder builder = new StringBuilder();

        builder.AppendLine("namespace Core.Functoids.UnitTests {");
        builder.AppendLine("using System;");
        builder.AppendLine("public class Test {");
        builder.Append(source);
        builder.AppendLine("}");
        builder.AppendLine("}");

        return builder.ToString();
    }

    public static Assembly CreateAssembly(string source)
    {
        CodeDomProvider provider = new CSharpCodeProvider();

        CompilerParameters options = new CompilerParameters();
        options.GenerateInMemory = true;
        options.GenerateExecutable = false;
        options.CompilerOptions = "/target:library";
        options.ReferencedAssemblies.Add("mscorlib.dll");
        options.ReferencedAssemblies.Add("System.dll");
        options.ReferencedAssemblies.Add("System.Xml.dll");

        CompilerResults results = provider.CompileAssemblyFromSource(options, new string[] { source });

        if (results.Errors.HasErrors)
        {
            StringBuilder exception = new StringBuilder();
            foreach (CompilerError error in results.Errors)
                exception.AppendFormat("{0}({1}): {2}\r\n", error.ErrorNumber, error.Line, error.ErrorText);
            throw new ApplicationException(exception.ToString());
        }

        return results.CompiledAssembly;
    }

    public static object Invoke(Assembly assembly, string method, params object[] args)
    {
        Type type = assembly.GetType("Core.Functoids.UnitTests.Test");
        object target = Activator.CreateInstance(type);

        return type.InvokeMember(method
        , BindingFlags.Instance
        | BindingFlags.Public
        | BindingFlags.InvokeMethod
        , null
        , target
        , args);
    }
}

The code shown above is pretty self-explanatory but let’s walk through it in more details.

In the unit-testing helper class, the GetInlineScriptBuffer method is responsible for calling the corresponding method on the compiled custom inline functoid class. With a bit of reflection, the corresponding custom functoid class is instantiated dynamically, and the method is invoked. You’ll notice a bunch of flags to deal with calling a non-public methodby reflection.

Next, the GenerateSourceCode method is responsible for wrapping the chunk of code returned from the previous call inside both a C# class and namespace declarations. This turns a string that represents a single function to the well-formed source code for a C# class.

Third, the CreateAssembly method is responsible for compiling the resulting source code to a managed assembly. Notice that this assembly is generated in memory and does not need to be cleaned up after the test is executed.

Finally, the Invoke method instantiates the compiled Core.Functoids.UnitTests.Test class and invokes the wrapped function – the one that has been extracted from the compiled version of the custom inline functoid. It is the job of the unit-test author to supply correct parameters when calling into this function, as we’ll see now.

Unit-Testing Custom Inline Functoids

The code shown hereafter is taken from a very simple custom functoid I once wrote in order to convert text representations of a datetime value from one pattern to another. This seems like a common occurence in the map I use. What’s more interesting is the RunDateTimeConversion method that makes use of the TestHelper class demonstrated in this post in order to call into the custom inline functoid emitted code.

using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class ConvertDateTime
{
    public static DateTime date_ = new DateTime(2012, 7, 14, 14, 59, 58, 000, DateTimeKind.Utc);
    public const String iso_date = "yyyy'-'MM'-'dd";
    public const String iso_date_time = "yyyy'-'MM'-'dd'T'HH':'mm':'ss";

    [TestMethod]
    public void TestConvertDate()
    {
        Assert.AreEqual(date_.Date, DateTime.Parse(RunDateTimeConversion("14/07/12", "dd/MM/yy", iso_date)).Date);
        Assert.AreEqual(date_.Date, DateTime.Parse(RunDateTimeConversion("14/07/2012", "dd/MM/yyyy", iso_date)).Date);
        Assert.AreEqual(date_.Date, DateTime.Parse(RunDateTimeConversion("14-07-2012", "dd-MM-yyyy", iso_date)).Date);
    }

    #region Implementation

    private string RunDateTimeConversion(string dateToConvert, string inputPattern, string outputPattern)
    {
        string inlineScriptBuffer = TestHelper.GetInlineScriptBuffer("Custom.Functoids.ConvertDateTime, My.Core.Functoids", 0, 0);
        string sourceCode = TestHelper.GenerateSourceCode(inlineScriptBuffer);

        Assembly assembly = TestHelper.CreateAssembly(sourceCode);
        return (string)TestHelper.Invoke(assembly, "DateTimeConversion", new object[] { dateToConvert, inputPattern, outputPattern });
    }

    #endregion
}

That’s all folks.

This entry was posted in BizTalk, Tips. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s