onsdag 1. mars 2017

Testing of custom pipeline components with Moq

Testing of custom pipeline components is not as hard as one might think. If one follows the standard way of testing such component as part of the pipeline where it is used, by running the provided with the BizTalk SDK tool Pipeline.exe, then it is quite possible that unit testing that component will be the last thing one would consider.
Well, that should definitely not be the case - make unit and assembly testing an integral part of your BizTalk development cycle!
In this article I will demonstrate you how easy it is to test a custom pipeline component without any dependency on external tools or even deployment to BizTalk server in a very simple yet very accessible and adoptable manner.

For this purpose I am utilizing one of the most widely used frameworks for mocking, called Moq. I am able to apply it in the case of pipeline components simply because the BizTalk pipeline framework is (thanks God) designed, and implemented, following solid SOA principles for componentization and separation of concern. Basically everything related to pipelines and pipeline components in BizTalk is defined by following the interface pattern. This automatically allows us to apply the principles of mocking by utilizing Moq. This framework gives the possibility to create mocked instances of interfaces and abstract classes with only few lines of code! And the best part of it - it is completely free! Developers all over the world totally love it!

So how mocking will help us in this case? Well, here is the definition of the main entry point method for a pipeline component defined in the interface IComponent:

IBaseMessage Execute(IPipelineContext pContext, IBaseMessage pInMsg)

The method accepts 2 parameters, both of types interface and returns an object of type interface too. Hence we can create mocks of the parameter objects and set those up in such a way that they behave as if they were passed by the BizTalk runtime. Lets go ahead and demonstrate this with a simple example.

One of the most often examples found on the Net for custom pipeline components is the one about diverting large files from entering the MessageBox database, by saving it to a dedicated location on a disc share and constructing a simple XML mesasage that contains the path to this file. For the demonstration purpose i have enhanced the component to also promote the original name of the file, as if it is used in CBR scenario. The component will return the newly created XML message as a result with the required property promoted.

Here is the corresponding code for this set up:
  
public IBaseMessage Execute(IPipelineContext pContext, IBaseMessage pInMsg)
{
    if (_largeFileLocation == null || _largeFileLocation.Length == 0)
        _largeFileLocation = Path.GetTempPath();
 
    if (_thresholdSize == null || _thresholdSize == 0)
        _thresholdSize = 4096;

    // Treat as large file only if the size of the file is greater than the thresholdsize
    if(pInMsg.BodyPart.GetOriginalDataStream().Length > _thresholdSize)
    {
        string largeFilePath = _largeFileLocation + pInMsg.MessageID.ToString() + ".msg";

        using(FileStream fs = new FileStream(largeFilePath, FileMode.Create))
        {

            // Write the message content to disk
         
            byte[] buffer = new byte[4096]; 
            int bytesRead  = 0;

            while((bytesRead = originalStream.Read(buffer, 0, buffer.Length)) > 0)
            {  
               fs.Write(buffer, 0, buffer.Length);
               fs.Flush();             
            }

            fs.Flush();
        }
      
        string originalFileName = (string)pInMsg.Context.Read("ReceivedFileName","http://www.microsoft.com/biztalk/schemas/file-properties");

        if(string.IsNullOrEmpty(originalFileName))
        {
            originalFileName = "NotAvailable";
        }
        else
        {
            originalFileName = System.IO.Path.GetFileName(originalFileName);
        }

        pInMsg.Context.Promote("OriginalLargeFileName", "www.example.com/largefile/properties/1.0", originalFileName);

        // Create the meta message
        string msgMeta = @"<LargeFile xmlns="http://example.com"><path>" + 
            largeFilePath + "</path></Largefile>";
      
        byte[] byteArray = System.Text.Encoding.UTF8.GetBytes(msgMeta);
        MemoryStream ms = new MemoryStream(byteArray);
        pInMsg.BodyPart.Data = ms;
     }

     return pInMsg; 
}

And this is how I am approaching the unit testing of the component.

// 1. Define the test method
[TestMethod]
[DeploymentItem(@"TestData\largefile.txt")]
public void TestLargeFileDecoder_HappyPath()
{

    // 2. Define the context mock<

    var contextMock = new Mock<IPipelineContext>();
    // The only thing that we really need to do with the context mock is to declare it as it is not used in the Execute() method at all.

    // 3. Define and setup the message context mock
    var messageContextMock = new Mock<IBaseMessageContext>();

    messageContextMock.Setup(c => c.Read("ReceivedFileName", this.FileAdapterNamespace))
        .Returns(@"\\somesrv\somedir\testfile123.txt");

    string originalFileName = null;

    messageContextMock.Setup(c => c.Promote(
        "OriginalFileName", 
         this.CustomPropertyNamespace, 
         It.Is<string>(
             s => s != null)))
            .Callback<string, string, object>( 
                (propName, namespace, value) => { originalFileName = value;});

    // 4. Define and setup the message part mock
    using(var fs = File.OpenRead("largefile.txt"))
    {
        var messagePartMock = new Mock<IBaseMassegaPart>();

        messagePartMock.Setup( mp => mp.GetOriginalDataStream()).Returns(fs);
        messagePartMock.SetupProperty(mp => mp.Data);

        var messageId = Guid.New();
        var messageMock = new Mock<IBaseMessage>();
        messsageMock.SetupProperty(m => m.MessageID, messageId);

       // 5. Define the message mock
       var messageId = Guid.New();
       var messageMock = new Mock<IBaseMessage>();

       messsageMock.SetupProperty(m => m.MessageID, messageId);
       messsageMock.SetupGet(m => m.Context).Returns(messageContextMock.Object);
       messsageMock.SetupGet(m => m.BodyPart).Returns(messagePartMock.Object);
 
       // 6. Invoke the Execute method
       var component = new LargeFileDecoder();
       component.LargeFileLocation = @"D:\Test\";
       
       var resultMessage = component.Execute(contextMock.Object, messageMock.Object);

       // 7. Verify the outcome:
       messageContextMock.Verify( c => c.Read("ReceivedFileName", this.FileAdapterNamespace), Times.Once()); 
 
       messageContextMock.Verify( c => c.Promote("OriginalFileName", this.CustomPropertyNamespace, 
           It.Is<string>(s => s == "testfile123.txt")), Times.Once());

       // Here we verify that the message produced is the meta message
           
       var resultMessageDoc = XDocument.Load(resultMessage.BodyPart.Data);
    
       // Verifying the root node name 
       Assert.AreEqual(XName.Get("LargeFile", "www.example.com/integration/demo/moq"), resultMessageDoc.Document.Name);

       // Extracting the path to the large file for verification
       string largeFilePath = resultMessageDoc.Document.Element("Path").Value;

       Assert.AreEqual(string.Format(@"D:\Test\{0}.msg", messageId), largeFilePath);
       
   }
}

That's really what it takes to create a pure unit test for your pipeline components.
Enjoy!

1 kommentar: