BizTalk: Unit test maps using mocks for extension objects
Introduction
Suppose that you have a BizTalk map that relies on database related functoids or external assemblies you would like to isolate using mocks/stubs to unit test, this blog shows you how.
Applies To
- BizTalk 2013 R2
Overview
BizTalk maps are internally converted to XSLT during BizTalk project compilation. Within the XSLT, all the BizTalk functoids defined in the map are converted to either a CSharp function within the script tag <msxsl:script> or referenced as external assemblies using extension objects defined in an extensions xml document. When the map is executed, BizTalk uses XslCompiledTransform to load up the xslt and transforms the source xml message.
XslCompiledTransform, when transforming, attempts to create an instance of the class, defined in the extension object, using the default parameter less constructor.
In order to mock any class methods in external assemblies defined in the extensions xml document, we just need to pass in pre-instantiated mock objects with methods whose signature exactly matches one used by the BizTalk map(the system under test).
Sample Code
This is the sample map that is used in this test.
When you validate this map in Visual Studio, the output XSLT looks like this. All functions prefixed with ScriptNS0 , mapped to the namespace http://schemas.microsoft.com/BizTalk/2003/ScriptNS0, belong to the class Microsoft.BizTalk.BaseFunctoids.FunctoidScripts.
<?xml version="1.0" encoding="UTF-16"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:var="http://schemas.microsoft.com/BizTalk/2003/var" exclude-result-prefixes="msxsl var s0 ScriptNS0" version="1.0" xmlns:s0="http://MockingBiztalkMaps/SourceSchema" xmlns:ns0="http://MockingBiztalkMaps/DestinationSchema" xmlns:ScriptNS0="http://schemas.microsoft.com/BizTalk/2003/ScriptNS0">
<xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
<xsl:template match="/">
<xsl:apply-templates select="/s0:Root" />
</xsl:template>
<xsl:template match="/s0:Root">
<xsl:variable name="var:v3" select="string(ValueToLookUp/text())" />
<ns0:Root>
<xsl:variable name="var:v1" select="ScriptNS0:DBLookup(0 , string(ValueToLookUp/text()) , "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WAREHOUSE;Data Source=localhost" , "dbo.CALENDAR" , "CALENDAR_ID")" />
<xsl:variable name="var:v2" select="ScriptNS0:DBValueExtract(string($var:v1) , "CALENDAR_DATE")" />
<xsl:attribute name="TranslatedValue">
<xsl:value-of select="$var:v2" />
</xsl:attribute>
<xsl:variable name="var:v4" select="ScriptNS0:DBLookup(0 , $var:v3 , "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WAREHOUSE;Data Source=localhost" , "dbo.CALENDAR" , "CALENDAR_ID")" />
<xsl:variable name="var:v5" select="ScriptNS0:DBErrorExtract(string($var:v4))" />
<Error>
<xsl:value-of select="$var:v5" />
</Error>
</ns0:Root>
<xsl:variable name="var:v6" select="ScriptNS0:DBLookupShutdown()" />
</xsl:template>
</xsl:stylesheet>
The map validation output also creates an extension xml file, and looks like this. This contains the mapping from the namespace to the class, in this instance the namespace http://schemas.microsoft.com/BizTalk/2003/ScriptNS0 is mapped to the FunctiodScripts class defined in the assembly Microsoft.BizTalk.BaseFunctiods.dll
<ExtensionObjects>
<ExtensionObject Namespace="http://schemas.microsoft.com/BizTalk/2003/ScriptNS0" AssemblyName="Microsoft.BizTalk.BaseFunctoids, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" ClassName="Microsoft.BizTalk.BaseFunctoids.FunctoidScripts" />
</ExtensionObjects>
In order to mock the methods used in the XSLT that belong to classes defined in the extensions, in this example methods DBLookup, DBValueExtract, DBErrorExtract and DBShutdown in class Microsoft.BizTalk.BaseFunctoids.FunctoidScripts, we need to pass a substitute mock object. This object should contain the signature for all the methods that we need to mock, and passed as an argument to XSLTCompiledTransform as the object to use for the namespace http://schemas.microsoft.com/BizTalk/2003/ScriptNS0.
This example below uses Moq to create mocks and Nunit as the test framework,
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Xsl;
using Map;
using Microsoft.XLANGs.BaseTypes;
using Moq;
using NUnit.Framework;
namespace Tests.UnitTests.Map
{
[TestFixture]
public class TestMapDbLookup
{
//Step 1: Define interface containing the signatures for all the functions you want to mock
/// <summary>
/// Defines the signature for all functions need to be mocked for unit testing MapDbLookup.
/// You can workout the signature of the assembly in the xslt output when you validate the map in Visual Studio
/// </summary>
public interface IMockSignaturesMapDbLookup
{
/// <summary>
/// Mock signature for Biztalk DbLookup functiod
/// </summary>
string DBLookup(int indexAutoGenInternal, string valueToLookUp, string connectionString, string tableName,
string columnToSearchLookupValueIn);
/// <summary>
/// Mock signature for Biztalk DB Value Extract functiod
/// </summary>
string DBValueExtract(string dbLookUpReturnIndex, string columnContainingTranslatedValue);
/// <summary>
/// Mock signature for Biztalk DB error functiod
/// </summary>
string DBErrorExtract(string dbLookUpReturnIndex);
/// <summary>
/// Mock signature for Biztalk internal DBLookupShutdown functiod
/// </summary>
string DBLookupShutdown();
}
[SetUp]
public void TestInit()
{
_sut = new MapDbLookup();
_mocksForMapDb = new Mock<IMockSignaturesMapDbLookup>();
//Step 2: Set up your mock object and map the namespace defined in the extensions xml file to this mock object
var nameSpace = GetNameSpaceForClass("Microsoft.BizTalk.BaseFunctoids.FunctoidScripts",
_sut.XsltArgumentListContent);
_namespaceToInstanceObjectsMap = new Dictionary<string, object> {{nameSpace, _mocksForMapDb.Object}};
}
//Step 3: The core work magic here
private static void Map(TransformBase map, string xmlInputFile, string outputXmlFile,
Dictionary<string, object> namespaceToInstanceObjectsMap)
{
//Construct XsltArgumentList mapping the namespace to the mocked object
var argumentsInstanceExtensionObjects = new XsltArgumentList();
foreach (var entry in namespaceToInstanceObjectsMap)
{
argumentsInstanceExtensionObjects.AddExtensionObject(entry.Key, entry.Value);
}
//Do the transform using XslCompiledTransform
var transform = new XslCompiledTransform();
var setting = new XsltSettings(false, true);
transform.Load(XmlReader.Create(new StringReader(map.XmlContent)), setting, new XmlUrlResolver());
using (var stream = new FileStream(outputXmlFile, FileMode.CreateNew))
{
//using XslCompiledTransform to transform the source to output message using mocked extension objects
transform.Transform(xmlInputFile, argumentsInstanceExtensionObjects, stream);
}
}
private Mock<IMockSignaturesMapDbLookup> _mocksForMapDb;
private MapDbLookup _sut;
private Dictionary<string, object> _namespaceToInstanceObjectsMap;
private const string ConnStrDoNotHardCodeThisInYourMap =
"Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=WAREHOUSE;Data Source=localhost";
//Your regular test method
[TestCase("SampleTesttData.xml", "18991231", "1899-12-31 00:00:00.000")]
public void ShouldMap(string inputXmlFile, string lookupValue, string mockDataTranslatedLookupValue)
{
//Arrange
inputXmlFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, inputXmlFile);
const int index = 0;
const string tableName = "dbo.CALENDAR";
const string searchColumnName = "CALENDAR_ID";
const string translatedValueColumnName = "CALENDAR_DATE";
//Set up mock..
//mock dblookup
_mocksForMapDb.Setup(
x => x.DBLookup(index, lookupValue, ConnStrDoNotHardCodeThisInYourMap, tableName, searchColumnName))
.Returns(index.ToString);
//mock dbvalue extract
_mocksForMapDb.Setup(x => x.DBValueExtract(index.ToString(), translatedValueColumnName))
.Returns(mockDataTranslatedLookupValue);
var actualOutputXmlFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
//mock dberrror
_mocksForMapDb.Setup(x => x.DBErrorExtract(index.ToString()))
.Returns("");
_mocksForMapDb.Setup(x => x.DBLookupShutdown())
.Returns("");
//Act
Map(_sut, inputXmlFile, actualOutputXmlFile, _namespaceToInstanceObjectsMap);
//Assert
//Can also do xml compare of the actual vs expected xml file..
Assert.IsTrue(File.Exists(actualOutputXmlFile));
}
private static string GetNameSpaceForClass(string classFullName, string xsltExtensionXml)
{
var root = XElement.Parse(xsltExtensionXml);
var namespaceOfScript =
from el in root.Descendants("ExtensionObject")
where (string) el.Attribute("ClassName") == classFullName
select el.Attribute("Namespace").Value;
return namespaceOfScript.First();
}
}
}
The sample xml file, SampleTestTData.xml, used in this test . The value to lookup 18991231 is mapped to 1899-12-31 00:00:00.000 in mock test data set up, in the test above.
<ns0:Root xmlns:ns0="http://MockingBiztalkMaps/SourceSchema">
<ValueToLookUp>18991231</ValueToLookUp>
</ns0:Root>
The output of xml file (found in %temp% location defined in test) generated by this test looks like this using the mocked object...
<ns0:Root TranslatedValue="1899-12-31 00:00:00.000" xmlns:ns0="http://MockingBiztalkMaps/DestinationSchema">
<Error></Error>
</ns0:Root>
Download Link
To download this source code, see github