onsdag 21. juni 2017

Testing of debatching scenarios in BizTalk with TransMock - part 2


This blog post is the second from the series of 2 posts about testing debatching scenarios in BizTalk server with TransMock. In the first post I described the fisrt, and default, mode for validating debatched messages received in a mock step instance - the serial mode. This mode allows one to apply the same set of validation steps to all the received messages in the test step.

In this post I will demonstrate how to utilize the cascading mode for validating messages produced as a result of de-batch processing in a BizTalk server flow. For the purpose of this post i will use the same test scenario as in the previous one. A quick recap of this one - a sales order that is released from an ERP system and sent for stock reservation in the warehouse  management system. After the reservation confirmation is received a shipment prepare request is sent to a logistics system. Again I will stick only to the happy path of this flow.

Lets assume again that for our particular test scenario there are 4 product lines in the test sales order. 
This time however I would like to perform different kind of validation on the individual WMS service request messages. I would like to validate that each message contains the required product line id, pallet identifier and requested quantity.

The code snippet below demonstrates how this is achieved:

        [TestMethod]
        [DeploymentItem(@"TestData\SO_ERP_4ProdLines.xml")]
        [DeploymentItem(@"TestData\WS_WMS_Response_Prod2.xml")]
        public void TestCascadingDebatching_HappyPath()
        {
            var testCase = new TestCase();
            testCase.Name = "TestCascadingDebatching_HappyPath";

            var sendSalesOrderStep = new MockSendStep()
            {
                Url = SOWarehouseReservationMockAddresses.Rcv_SO_ERP_Q,
                Encoding = "UTF-8",
                RequestPath = "SO_ERP_4ProdLines.xml"
            };

            var receiveWMSReservationStep = new MockRequestResponseStep()
            {
                Url = SOWarehouseReservationMockAddresses.Snd_WMS_WCF_WebAPI,
                Encoding = "UTF-8",
                ResponsePath = "WS_WMS_Response_Prod1.xml",
                RunConcurrently = true,
                DebatchedMessageCount = 4,
                ValidationMode = MultiMessageValidationMode.Cascading
            };

            #region First WS invocation validation
            var wmsReservationValidationStep1 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateWMSReservationRequest(data, "5500096856", "122300005894-6845-3", "33.0")
            };

            var firstWSValidationCollection = new System.Collections.ObjectModel.Collection();

            firstWSValidationCollection.Add(wmsReservationValidationStep1);

            receiveWMSReservationStep.CascadingSubSteps.Add(0, firstWSValidationCollection);
            #endregion            
            
            #region Second WS invocation validation
            var wmsReservationValidationStep2 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateWMSReservationRequest(data, "5500096856", "122300005894-6845-3", "123.33")
            };

            var secondWSValidationCollection = new System.Collections.ObjectModel.Collection();

            secondWSValidationCollection.Add(wmsReservationValidationStep2);

            receiveWMSReservationStep.CascadingSubSteps.Add(1, secondWSValidationCollection);
            #endregion

            #region Third WS invocation validation
            var wmsReservationValidationStep3 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateWMSReservationRequest(data, "5500096856", "122300008765-1002-22", "2.58")
            };

            var thirdWSValidationCollection = new System.Collections.ObjectModel.Collection();

            secondWSValidationCollection.Add(wmsReservationValidationStep2);

            receiveWMSReservationStep.CascadingSubSteps.Add(2, thirdWSValidationCollection);
            #endregion

            #region Fourth WS invocation validation
            var wmsReservationValidationStep4 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateWMSReservationRequest(data, "5500096856", "122300099890-2589-33", "8.0")
            };

            var fourthWSValidationCollection = new System.Collections.ObjectModel.Collection();

            secondWSValidationCollection.Add(wmsReservationValidationStep4);

            receiveWMSReservationStep.CascadingSubSteps.Add(3, fourthWSValidationCollection);
            #endregion            

            testCase.ExecutionSteps.Add(receiveWMSReservationStep);

            var receiveShipClearanceStep = new MockReceiveStep()
            {
                Url = SOWarehouseReservationMockAddresses.Snd_Logistics_Q,
                Encoding = "UTF-8",
                RunConcurrently = true,
                DebatchedMessageCount = 4,
                ValidationMode = MultiMessageValidationMode.Cascading
            };

            #region First ship clearance validation step
            var shipClearValidationStep1 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateShipmentClearance(data, "Shipment ref", "product id", "status code")                
            };

            var firstShipValidationCollection = new System.Collections.ObjectModel.Collection();

            firstShipValidationCollection.Add(wmsReservationValidationStep4);

            receiveShipClearanceStep.CascadingSubSteps.Add(0, firstShipValidationCollection);
            #endregion

            #region Second ship clearance validation step
            var shipClearValidationStep2 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateShipmentClearance(data, "Shipment ref", "product id", "status code")
            };

            var secondShipValidationCollection = new System.Collections.ObjectModel.Collection();

            secondShipValidationCollection.Add(shipClearValidationStep2);

            receiveShipClearanceStep.CascadingSubSteps.Add(1, secondShipValidationCollection);
            #endregion

            #region Third ship clearance validation step
            var shipClearValidationStep3 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateShipmentClearance(data, "Shipment ref", "product id", "status code")
            };

            var thirdShipValidationCollection = new System.Collections.ObjectModel.Collection();

            thirdShipValidationCollection.Add(shipClearValidationStep3);

            receiveShipClearanceStep.CascadingSubSteps.Add(2, thirdShipValidationCollection);
            #endregion

            #region Fourth ship clearance validation step
            var shipClearValidationStep4 = new TransMock
                .Integration.BizUnit.Validation.LambdaValidationStep()
            {
                ValidationCallback = (data) =>
                    ValidateShipmentClearance(data, "Shipment ref", "product id", "status code")
            };

            var fourthShipValidationCollection = new System.Collections.ObjectModel.Collection();

            fourthShipValidationCollection.Add(shipClearValidationStep4);

            receiveShipClearanceStep.CascadingSubSteps.Add(3, fourthShipValidationCollection);
            #endregion

            testCase.ExecutionSteps.Add(receiveShipClearanceStep);

            var testRunner = new BizUnit.BizUnit(testCase);

            testRunner.RunTest();
        }

        private bool ValidateWMSReservationRequest(
            System.IO.Stream data,
            string orderId,
            string productId,
            string quantity)
        {
            // Loading the document from the data stream
            var msgDoc = XDocument.Load(data);

            ValidateCommonData(msgDoc, orderId);

            // Here we add the validation logic for the ordered quantity element
            var xProductId = msgDoc.Root.Element("ProductLine")
                .Element("ProductId");

            Assert.IsNotNull(xProductId, "No OrderID element found");
            Assert.AreEqual(xProductId.Value, productId, "Ordered value not as expected");

            // Here we add the validation logic for the ordered quantity element
            var xOrderedQuantity = msgDoc.Root.Element("ProductLine")
                .Element("OrderedQty");

            Assert.IsNotNull(xOrderedQuantity, "No OrderID element found");
            Assert.AreEqual(xOrderedQuantity.Value, quantity, "Ordered value not as expected");

            return true;
        }

        private void ValidateCommonData(XDocument msgDoc, string orderId)
        {
            //fetch the order Id from the message
            var xErderId = msgDoc.Root.Elements("OrderID").FirstOrDefault();

            Assert.IsNotNull(xErderId, "No OrderID element found");
            Assert.AreEqual(xErderId.Value, orderId, "OrderID value not as expected");
         
        }

        private bool ValidateShipmentClearance(
            System.IO.Stream data, 
            string shipRef, 
            string productId, 
            string statusCode)
        {
            // Loading the document from the data stream
            var msgDoc = XDocument.Load(data);

            //fetch the shipment ref Id from the message
            var xShipmentRef = msgDoc.Root.Elements("ShipRef").FirstOrDefault();

            Assert.IsNotNull(xShipmentRef, "No ShipRef element found");
            Assert.AreEqual(xShipmentRef.Value, shipRef, "ShipRef value not as expected");

            //fetch the shipment ref Id from the message
            var xProductId = msgDoc.Root.Elements("ProductId").FirstOrDefault();

            Assert.IsNotNull(xProductId, "No ProductId element found");
            Assert.AreEqual(xProductId.Value, productId, "ProductId value not as expected");

            //fetch the shipment ref Id from the message
            var xStatusCode = msgDoc.Root.Elements("Status").FirstOrDefault();

            Assert.IsNotNull(xStatusCode, "No Status element found");
            Assert.AreEqual(xStatusCode.Value, statusCode, "Status value not as expected");

            return true;
        }


Note the following properties that are set on receiveWMSReservationStep
  • DebatchedMessageCount = 4 - this property is set to the number of messages that we expect to receive from BizTalk in this step.
  • ValidationMode = MultiMessageValidationMode.Cascading - here we set the validation mode to Cascading.
The most noticable difference however compared to the test with validation in Serial mode is its size. Adding dedicated validation steps for each message simply requires more lines of code :)
The essential part in the cascading validation mode for de-batched messages is that for each message received in the mock step instance there will be executed a dedicated set of validation steps. These steps are added to the dedicated dictionary property called CascadingSubSteps. The key in this dictionary is an integer that corresponds to the index of the received message. Its value should be in the range of 0 and DebatchedMessageCount - 1. The value in the dictionary elements is a collection of BizUnit SubStep objects that contains the various validation steps for the particular message. 

In the current test case we have 4 different instances of the LambdaValidationStep step. Each instance is then added to a dedicated collection of SubSteps before it being added as a value to the dictionary. Each validation step points to the same private method but passes different values that will be used in the verification process. This method verifies that the order number, the product Id and the ordered quantity are as expected on each product line.

Similar is the case with the mock receive step for the ship prepare message to the logistics system. There we have also defined that we expect 4 messages and we have defined 4 different validation steps of type LambdaValidationStep which are added to the CascadingSubSteps dictionary. There we check if the ShipRef, ProductId and Status elements is in place and that they are containing the expected values.

Conclusion

With this method it becomes relatively simple to define test cases that re-use the definition of a single mock step to receive and validate multiple de-batched messages. The Cascading mode gives extra flexibility in validation each individually received message in a unique way thus allowing us to be even more stringent in checking the output of an integration flow.

onsdag 14. juni 2017

Testing of debatching scenarios in BizTalk with TransMock - part 1


This blog post is a series of 2 posts about testing debatching scenarios in BizTalk server with TransMock. Why exactly 2? Simply because TransMock has 2 different modes for verifying messages which are received as part of a debatching operation - serial and cascading.
  • Serial mode, which is the default one, is where each and every debatched message is validated in the same way by the same validation step. 
  • Cascading mode is when each debatched message is validated through a dedicated validation step.
In this post I will demonstrate how to utilize the serial mode for testing and verifying batching scenarios. But first let's describe the test scenario. The flow is about a sales order that is released from the ERP and sent for verification of warehouse availability by the warehouse management system.
BizTalk receives an XML message from the ERP, which is debatched in the receive pipeline. Each individual message is then processed by a subscribed orchestration. In the orchestration the request message is mapped to a web service request and then the corresponding web service exposed by the WMS system is invoked. The response from the service is then transformed to a final message type which is put onto a queue to the target logistics system.
The input message contains the sales order with separate product lines, which are debatched. Lets assume that for a particular test scenario there are 4 product lines in the test sales order.
The web service is invoked to verify the availability of the product quontity of each product line.

By now it should be obvious that there is a particular challange in testing this flow with TransMock. The web service endpoint mock should be invoked multiple number of times - in our test case that is 4. How shall the test case be defined in this particular scenario? The answer to this is pretty simple as TransMock supports debatching scenarios out of the box since version 1.2. The code snippet below demonstrates how is this achieved:

    
    [TestMethod]
    [DeploymentItem(@"TestData\SO_ERP_4ProdLines.xml")]
    [DeploymentItem(@"TestData\WS_WMS_Response_Prod1.xml")]
    public void TestSerialDebatching_HappyPath()
    {
        var testCase = new TestCase();
        testCase.Name = "TestSerialDebatching_HappyPath";
    
        var sendSalesOrderStep = new MockSendStep()
        {
            Url = SOWarehouseReservationMockAddresses.Rcv_SO_ERP_Q,
            Encoding = "UTF-8",
            RequestPath = "SO_ERP_4ProdLines.xml"
        };

        var receiveWMSReservationStep = new MockRequestResponseStep()
        {
            Url = SOWarehouseReservationMockAddresses.Snd_WMS_WCF_WebAPI,
            Encoding = "UTF-8",
            ResponsePath = "WS_WMS_Response_Prod1.xml",
            RunConcurrently = true,
            DebatchedMessageCount = 4,
            ValidationMode = MultiMessageValidationMode.Serial              
        };          

        var wmsReservationValidationStep = new TransMock
            .Integration.BizUnit.Validation.LambdaValidationStep()
        {
            ValidationCallback = (data) => {
                // Loading the document from the data stream
                var msgDoc = XDocument.Load(data);

                //fetch the order Id from the message
                var orderId = msgDoc.Root.Elements("OrderID").FirstOrDefault();
                 
                Assert.IsNotNull(orderId, "No OrderID element found");
                Assert.AreEqual(orderId.Value, "5500096856", "OrderID value not as expected");

                return true;
            }
        };

        receiveWMSReservationStep.SubSteps.Add(wmsReservationValidationStep);
        testCase.ExecutionSteps.Add(receiveWMSReservationStep);

        var receiveShipClearanceStep = new MockReceiveStep()
        {
            Url = SOWarehouseReservationMockAddresses.Snd_Logistics_Q,
            Encoding = "UTF-8",              
            RunConcurrently = true,
            DebatchedMessageCount = 4,
            ValidationMode = MultiMessageValidationMode.Serial
        };

        var shipClearValidationStep = new TransMock
            .Integration.BizUnit.Validation.LambdaValidationStep()
        {
            ValidationCallback = (data) =>
            {
                // Loading the document from the data stream
                var msgDoc = XDocument.Load(data);

                //fetch the order Id from the message
                var shipmentRef = msgDoc.Root.Elements("ShipRef").FirstOrDefault();

                Assert.IsNotNull(shipmentRef, "No ShipRef element found");
                Assert.AreEqual(shipmentRef.Value, "1223/5500096856", "ShipRef value not as expected");

                return true;
            }
        };
 
        receiveShipClearanceStep.SubSteps.Add(shipClearValidationStep);
        testCase.ExecutionSteps.Add(receiveShipClearanceStep);

        var testRunner = new BizUnit.BizUnit(testCase);

        testRunner.RunTest();
    }

Note the following properties that are set on the inventoryWebServiceMockStep:
- DebatchedMessageCount = 4 - this property is set to the number of messages that we expect to receive from BizTalk in this step.
- ValidationMode = MultiMessageValidationMode.Serial - this property is about setting the validation mode for debatched messages. This is also the default value of this property. Here it is set just for the purpose of demosntration, otherwise it is not required.

The essential part in the serial validation mode for debatched messages is that for each debatched message received in the mock step instance there will be executed each and every validation step defined in the SubSteps collection.

In our case we have only one LambdaValidationStep instance. This validation step verifies that the order number is as expected on each product line. If the method of using the LambdaValidationStep is new to you, please read the dedicated blog post for it here. LambdaValidationStep was introduced in version 1.2 as an attempt to promote a more flexible, simplified and unified validation programming model for validation logic in BizUnit.

The same is the case with the mock receive step for the ship prepare message to the logistics system. There we have also defined that we expect 4 messages and we have defined one step of type LambdaValidationStep. There we check if the ShipRef element is in place and contains the expected value.

Conclusion

With this method it is relatively simple to define test cases that re-use the same definition of a single mock step to receive and validate multiple debatched messages. In fact this technique is applicable not only in classical debatching scenarios as described in the fictive test case above, but in cases where there are for example looping conditions in the tested flow, resulting in multiple invocations of a single mock step instance.

Jump to the Testing of debatching scenarios in BizTalk with TransMock - part 2 blog post to learn more about the Cascading validation mode