mandag 27. april 2015

Testing a real world BizTalk integration with TransMock - Part 2

This post is the second one in the series "Testing real world BizTalk integration with TransMock".  It describes the creation of a test case and covers the flow of messages between the ERP and WMS systems that implement the goods reservation.
After all the wiring is performed  as explained in the first post, and the solution is deployed to the local BizTalk server instance, it is time to create our first test case. The process of creating a test case is divided in 2 steps - preparation and implementation. In the preparation step I will create the test class and prepare all the files that contain the different messages which will be sent to BizTalk server by the various test steps. The second step is covering the actual coding of the test. 

Preparation

First I start by adding a class called SOProcurementTests to the IntegrationTests project. There should be added references to the BizUnit.dll, BizUnit.Xaml.dll and TransMock.Integration.BizTalk.dll assemblies to that project. The latter one is located in the Ext folder under the TransMock installation folder.
After this I add the SOProcurementMockAddresses.cs class file to the IntegrationTests project. If you recall this file was generated by the Mockifier during deployment of the solution and should be located in the folder wheter the IntegrationTests project resides.
The test case I create here will be testing the happy path of execution and is therefore called NewSO_HappyPath. 

Now it is time to add to the test project the files that the test case will be sending to BizTalk server. From the flow diagram in the first post we can  identify that for the purpose of the goods reservation message flow we have the following messages that are sent from a system to BizTalk server:
  • SO created message - XML message sent from the ERP to BizTalk
  • Goods reservation response message - SOAP message sent from the WMS system to BizTalk
For the purpose of testing with TransMock we store these messages in corresponding files and then use those files from within the test. For this purpose I add a folder to the project called TestData and add 2 files to it that will contain each message correspondingly. These files are called respectively:
  • NewSO_HappyPath.xml - contains the XML for a new sales order 
  • WMSReservationResponse_HappyPath.xml - contains the SOAP XML with the warehouse reservation response
Note the naming convention here denoting that these files are related to testing a happy path scenario. This is a good practice for naming the message files as over time with introducing more tests it might become difficult to manage those. So give this a thought and come up with some naming scheme that suits your needs before there are too many files and test cases which might make your life miserable.

In this case the SO created message is a standard Dynamics AX entity XML for a sales order. A cut down version of the contents of this file, emphasizing on the important to the integration fields, is shown below:
NewSO.xml contents
….
<?xml version="1.0" encoding="utf-8" ?> 
<Envelope xmlns="http://schemas.microsoft.com/dynamics/2011
    01/documents/Message">
    <Header>
        <MessageId>{2002A291-8AA4-4405-BC10-383C30376F76}</MessageId>
        <Action>http://schemas.microsoft.com/dynamics/
            2008/01/services/SalesOrderService/insert</Action>
        <RequestMessageId>{13A5A1E4-D248-4361-B484-791AE121C952}
            </RequestMessageId> 
    </Header>
    <Body>
        <MessageParts xmlns="http://schemas.microsoft.com/dynamics/
            2011/01/documents/Message">
            <SalesOrder xmlns="http://schemas.microsoft.com/dynamics/
                2008/01/documents/SalesOrder">
                <DocPurpose>Original</DocPurpose>
                <SenderId>DAT</SenderId>
                <SalesTable class="entity">
                    <_DocumentHash>de321661b44419fc5052246e6274f726
                        </_DocumentHash>
                    ...
                    <CurrencyCode>EUR</CurrencyCode>
                    <CustAccount>4005</CustAccount>
                    <CustGroup>40</CustGroup>
                    <Deadline>2002-03-31</Deadline>
                    <DeliveryAddress>St. Hallvard vei 1 N-0244 
                        Oslo Norway</DeliveryAddress>
                    <DeliveryCity>Oslo</DeliveryCity>
                    <DeliveryCountryRegionId>NO
                        </DeliveryCountryRegionId
                    <DeliveryDate>2002-03-01</DeliveryDate>
                   ...
                    <DeliveryName>Office Supplies Inc.</DeliveryName>
                    ...
                    <DlvMode>UPS</DlvMode>
                   ...
                    <SalesGroup>CSG-OTH</SalesGroup>
                    <SalesId>00016_036</SalesId>
                    <SalesName>Office Supplies Inc.</SalesName>
                    <SalesPoolId>DEF</SalesPoolId>
                    <SalesResponsible>MPO</SalesResponsible>
                    <SalesStatus>Backorder</SalesStatus>
                    <SalesTaker>TJO</SalesTaker>
                    <SalesType>Sales</SalesType>
                    <SettleVoucher>None</SettleVoucher>
                    ...
                    <totalBalance>3300.00</totalBalance>
                    <TotalCashDiscount>33.00</TotalCashDiscount>
                    <TotalInvoice>3300.00</TotalInvoice>
                        <SalesLine class="entity">
                            ...
                            <Complete>No</Complete>
                            <ConfirmedDlv>2002-03-01</ConfirmedDlv>
                            <CurrencyCode>EUR</CurrencyCode>
                            <CustAccount>4005</CustAccount>
                            ...
                            <DeliveryType>None</DeliveryType>
                            <DeliveryZipCode>N-0244</DeliveryZipCode>
                            <InventDimId>00001_060</InventDimId>
                            <InventRefType>None</InventRefType>
                            <InventTransId>00065_059</InventTransId>
                            <ItemId>OL-1500</ItemId>
                          ...
                            <LineAmount>3300.00</LineAmount>
                            <LineNum>1.0000000000</LineNum>
                            <Name>Office Lamp 1500 2-tubes</Name>
                            <PalletTagging>No</PalletTagging>
                            <PriceUnit>1.00</PriceUnit>
                            <ProjCategoryId>Lamps</ProjCategoryId>
                            <QtyOrdered>200.00</QtyOrdered>
                            ...
                            <Reservation>None</Reservation>
                            ...
                            <SalesGroup>CSG-OTH</SalesGroup>
                            <SalesId>00016_036</SalesId>
                            <SalesPrice>16.50</SalesPrice>
                            <SalesQty>200.00</SalesQty>
                            <SalesStatus>Backorder</SalesStatus>
                            <SalesType>Sales</SalesType>
                            <SalesUnit>Pcs</SalesUnit>
                            <Scrap>No</Scrap>
                            <TaxAutogenerated>Yes</TaxAutogenerated>
                            <TaxItemGroup>full</TaxItemGroup>
                            ...
                        </SalesLine>
                    </SalesTable>
            </SalesOrder>
        </MessageParts>
    </Body>
</Envelope>

The WMS reservation response is from a custom web service exposed by the WMS system and is much smaller in size. It looks like this in its entirety:
WMSReservationResponse_HappyPath.xml
<s:Envelope xmlns:s="http://www.w3c.org/standards/SOAP-XML/2001">
 <s:Header>
 ....
 </s:Header>
 <s:Body>
  <wms:reserveGoodsResponse xmlns:wms="WMS Service namespace here">
  <operation>RSVNRL</operation>
  <respCode>100</respCode>
  <extRef>00016_036</extRef>
  <items>
   <item>
    <id>00001_060</id>
    <inventLocId></inventLocId>
    <resultCode>1</resultCode>
    <item>
  </items>
  </wms:reserveGoodsResponse>
 </s:Body>
</s:Envelope>

At last in order for the test case to be able to use these files there should be added the following attributes to the test method definition:

[DeploymentItem(@"TestData\NewSO_HappyPath.xml")]
[DeploymentItem(@"TestData\WMSReservationResponse_HappyPath.xml")]
public void NewSO_HappyPath()
{
}

You have to make sure that these files are always copied to the deployment folder used under test execution. The easiest way to do this is to set the Build action in the Properties window for each file to be Copy Always
With this all the preparation for the test case is done and we can carry out with the actual implementation.

Test implementation

The first thing I will do is to add a definition of a BizUnit test cse to the test method:
public void NewSO_HappyPath()
{
    TestCase testCase = new TestCase("NewSO_HappyPath");
    ...
Then I will add the first test step that will send the SO message to the ERP_SOCreateIn_MSMQ receive location and with this it will initiate the process. The major difference from a conventional BizUnit test case when using TransMock is that only 4 distinct test step types are used throughout the entire test. These test steps are making it possible to directly communicate with a corresponding receive location or send port which are executing the mock adapter. These steps are referred to as the Mock* steps and are defined in the TransMock.Integration.BizUnit namespace.
So, for the purpose of sending a one-way message to the ERP_SOCreateIn_MSMQ receive location I will use the MockSendStep. The code snippet below shows how it will be set up:

var erpSendNewSalesOrderStep = new MockSendStep()
     {
         Url = SalesOrderProcurementMockAddresses.ERP_SOCreateIn_MSMQ,
         Encoding = "UTF-8",
         RequestPath = "NewSO_SingleLine_HappyPath.xml"
     };

     testCase.ExecutionSteps.Add(erpSendNewSalesOrderStep);

Notice how the URL property of the step is set - this is where the helper class generated by the Mockifier comes in handy. In this particular case we are basically assigning the value of a property having the same name as the receive location to which the message should be sent. This is a very convenient way of setting the correct URL - for each mocked receive location and send port there is a corresponding get property in this helper class with the exact same name which returns the corresponding mock URL.
Another property that is required to be set on this step is the RequestPath - it must be set to the path of a file that contains the message that the step will be sending to BizTalk. In this particular case we set it to the NewSO_HappyPath.xml which contains the SO created message.
The Encoding property is also required and it should be set to the encoding in which the message shall be serialized and sent to the corresponding receive location.
The Timeout property is an optional one and is set to the number of seconds for which the operation would wait. In case the message is not sent for the defined timeout an exception is thrown by the step.

The next step is to define the test step that will act as the web service method for the reservation in the warehouse management system. Note that we will only emulate this web method alone and not spin any web service stub on the fly! This is fundamental in using TransMock that makes it super easy to test even the most complex scenarios. For this purpose we will utilize the MockRequestResponseStep which receives a request message from BizTalk and sends a synchronous response back. We will be verifying the received request and will be sending the expected response from a file. The code snippet below shows how this is done:
    ….
    SchemaDefinition wmsRequestSchema = new SchemaDefinition(){
        XmlSchemaNameSpace = "http://somewms.com/services/ManamgementService/v12",
        XmlSchemaPath = @"..\..\..\Schemas\WMS\WMS_ManagementService.xsd"
    };

    var wmsGoodsReservationRequestValidationStep = new XmlValidationStep();
            
    wmsGoodsReservationRequestValidationStep
      .XmlSchemas.Add(wmsRequestSchema);
            
    var xpathNumOfItems = new XPathDefinition(){
        XPath = "count(/*[local-name()='reserveGoods' and 
        namespace-uri()='http://somewms.com/services/ManamgementService/v12']/*[local-name()='items' and 
        namespace-uri()='']/*[local-name()='item' and namespace-uri()=''])",
        Value = "1"
    };

    wmsGoodsReservationRequestValidationStep
        .XPathValidations.Add(xpathNumOfItems);

    var xpathItemCommand = new XPathDefinition()
    {
         XPath = "/*[local-name()='reserveGoods' and 
             namespace-uri()='http://somewms.com/services/ManamgementService/v12']
             /*[local-name()='items' and namespace-uri()='']
             /*[local-name()='item' and namespace-uri()='' and position()='1']
             /*[local-name()='cmd' and namespace-uri()='']/text()",
         Value = "RSRV"
    };

    wmsGoodsReservationRequestValidationStep
        .XPathValidations.Add(xpathItemCommand);

    var wmsGoodsReservationRequestResponseStep = new MockRequestResponseStep()
    {
        Url = SalesOrderProcurementMockAddresses.WMS_ManagementService_WCF,
        Encoding = "UTF-8",
        ResponsePath = "WMS_ReservationResponse_SingleLine_HappyPath.xml",
        Timeout = 10
    };

    wmsGoodsReservationRequestResponseStep
        .SubSteps.Add(wmsGoodsReservationRequestValidationStep);
    
    testCase.ExecutionSteps.Add(wmsGoodsReservationRequestResponseStep);
The main difference with the first step that we created earlier is that here we set the URL property to a different value and that we set the ResponsePath property with the path to the file that contains the actual response message. 

In addition we configure and set an XmlValidatorStep that will execute several XPath validations against the request that was received. In particular these are:
1. The count of the nodes /reserveGoods/items/item is equal to 1
2. The value of the element /reserveGoods/items/item[1]/cmd is RSRV
Finally we set up the test step that mocks the reception of the reservation confirmation message by the ERP system. For this purpose we utilize the MockReceiveStep which implements a one way receive operation. Setting up this step is pretty straight forward and is shown in the snippet below:
….
var erpSalesOrderSchemaDefinition = new SchemaDefinition()
{
    XmlSchemaNameSpace = "http://schemas.microsoft.com/dynamics/2011/01/documents/Message",
    XmlSchemaPath = @"..\..\..\Schemas\ERP\MessageEnvelope.xsd"
};

var erpSOReservationConfirmValidationStep = new XmlValidationStep();

erpSOReservationConfirmValidationStep.XmlSchemas.Add(erpSalesOrderSchemaDefinition);

var xpathItemDimensionCount = new XPathDefinition()
{
    XPath = "count(/*[local-name()='Envelope' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2011/01/documents/Message']
    /*[local-name()='Body' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2011/01/documents/Message']
    /*[local-name()='MessageParts' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2011/01/documents/Message']
    /*[local-name()='SalesOrder' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder']
    /*[local-name()='SalesTable' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder']
    /*[local-name()='SalesLine' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder']
    /*[local-name()='InventDim' and 
        namespace-uri()='http://schemas.microsoft.com/dynamics/2008/01/documents/SalesOrder'])",
    Value = "1"
};

erpSOReservationConfirmValidationStep
    .XPathValidations.Add(xpathItemDimensionCount);

var erpSOReservationConfirmStep = new MockReceiveStep()
{
    Url = SalesOrderProcurementMockAddresses.ERP_SOReservationConfirm_MSMQ,
    Encoding = "UTF-8",
    Timeout = 10
};

erpSOReservationConfirmStep.SubSteps
    .Add(erpSOReservationConfirmValidationStep);

testCase.ExecutionSteps.Add(erpSOReservationConfirmStep);
Again the important things to notice here is that first of all we do not have any sort of path related property to set here - the received message is simply consumed by the step and passed along any validation steps that might be configured, as is the case here. And of course the URL property is set for the corresponding send port.

The message that we receive in this step from BizTalk is a modified SO message with fewer fields carrying specific updated values indicating that the ordered goods have been reserved. The validation step is set to evaluate the following field:
        1. The count of /Envelope/Body/MessageParts/SalesOrder/SalesTable/SalesLine/InventDim is 1

And at the very end of the test method we add the 2 lines that will execute the entire test case

BizUnit.BizUnit testRunner = new BizUnit.BizUnit(testCase);
testRunner.RunTest();

In the next post you can read on how the shipment creation flow is tested. Continue to the next Post 3

1 kommentar: