Generating ESQL Unit Tests

One of the great features of Sonarqube is that it gives visibility to your technical debt.

This importantly includes test coverage.

One could argue that without test coverage there isn’t as much value in looking at the issues that Sonarqube
reports as without tests, there are few guarantees that making changes to the code won’t introduce a regression issue.

So we have been working with teams on how to get tests for IIB.

Developers and testers can test a flow end to end, and it is valuable to collect that interaction as an automated test or integrated test.

But once you start generating test coverage, you start looking for the gaps.
Integration tests are valuable is that they are more closely aligned to user or systems needs, and the end interaction helps confirm requirements.

But they are hard to setup. So we’ve been helping teams with Unit testing.

The code below shows a hard to test version of the code.
To test this code, the Input and Output needs to setup and read, which is hard to do. It is also hard to have more then once test as they are more likely to interact and be more fragile.

/* Smaple ploject */
BROKER SCHEMA COBOLApplication

CREATE COMPUTE MODULE OrderList2PurchaseData_Compute
  CREATE FUNCTION Main() RETURNS BOOLEAN
  BEGIN
    CALL CopyMessageHeaders();

    SET OutputRoot.DFDL.PurchaseData.InvoiceCount = InputRoot.DFDL.OrderList.InvoiceCount;
    SET OutputRoot.DFDL.PurchaseData.ItemCount = InputRoot.DFDL.OrderList.ItemCount;

    DECLARE TotalQuantity INTEGER 0;
    DECLARE I INTEGER 1;
    DECLARE J INTEGER;
    DECLARE InputSaleListRef REFERENCE TO InputRoot.DFDL.OrderList.SaleList;
    SET J = CARDINALITY(InputSaleListRef.Invoice[]);
    WHILE I <= J DO
      DECLARE InputSaleListInvoiceRef REFERENCE TO InputSaleListRef.Invoice[I];
      SET OutputRoot.DFDL.PurchaseData.SaleList.Invoice[I].CustomerInitial[1] = InputSaleListInvoiceRef.InvoiceHeader.CustomerInitial[1];
      DECLARE OutputSaleListInvoiceRef REFERENCE TO OutputRoot.DFDL.PurchaseData.SaleList.Invoice[I];
      SET OutputSaleListInvoiceRef.CustomerInitial[2] = InputSaleListInvoiceRef.InvoiceHeader.CustomerInitial[2];
      SET OutputSaleListInvoiceRef.Surname = InputSaleListInvoiceRef.InvoiceHeader.Surname;

      DECLARE K INTEGER 1;
      DECLARE L INTEGER;
      SET L = CARDINALITY(InputSaleListInvoiceRef.Items.Item[]);

     WHILE K <= L DO
       DECLARE OutputItem ROW;

         SET OutputItem = THE (
         SELECT Inv.ItemType
         , Inv.ItemCode
        , Inv.Description
        , Inv.Price
        , Inv.Category
        , It.Quantity
        FROM InputSaleListInvoiceRef.Items.Item[K] AS It
        , InputRoot.DFDL.OrderList.Inventory.Item[] AS Inv
        WHERE Inv.ItemCode = It.ItemCode
        &AND Inv.ItemCode = InputSaleListInvoiceRef.Items.Item[K].ItemCode
        );

       IF OutputItem.ItemType = ’1′ THEN
         SET OutputSaleListInvoiceRef.Items[K].ItemType = OutputItem.ItemType;
         SET OutputSaleListInvoiceRef.Items[K].Item1.ItemCode = OutputItem.ItemCode;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Description = OutputItem.Description;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Price = OutputItem.Price;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Category = OutputItem.Category;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Quantity = OutputItem.Quantity;
       ELSE
        SET OutputSaleListInvoiceRef.Items[K].ItemType = OutputItem.ItemType;
        SET OutputSaleListInvoiceRef.Items[K].Item2.ItemCode = OutputItem.ItemCode;
        SET OutputSaleListInvoiceRef.Items[K].Item2.Colour = OutputItem.Description;
        SET OutputSaleListInvoiceRef.Items[K].Item2.Price = OutputItem.Price;
        SET OutputSaleListInvoiceRef.Items[K].Item2.Quantity = OutputItem.Quantity;

      END IF;

      SET TotalQuantity = TotalQuantity + OutputItem.Quantity;
      SET K = K + 1;
    END WHILE;

    SET I = I + 1;
  END WHILE;

  –Trailer
  SET OutputRoot.DFDL.PurchaseData.Trailer.TotalQuantity = TotalQuantity;

  IF EXISTS(InputRoot.DFDL.OrderList.PointOfSale1[]) THEN
    SET OutputRoot.DFDL.PurchaseData.Trailer.PointOfSaleType = ’1′;
  ELSE
    SET OutputRoot.DFDL.PurchaseData.Trailer.PointOfSaleType = ’2′;
  END IF;

  RETURN TRUE;
  END;

  CREATE PROCEDURE CopyMessageHeaders() BEGIN
    DECLARE I INTEGER 1;
    DECLARE J INTEGER;
    SET J = CARDINALITY(InputRoot.*[]);
    WHILE I < J DO
      SET OutputRoot.*[I] = InputRoot.*[I];
      SET I = I + 1;
    END WHILE;
  END;
END MODULE;

 

The refactoring tool takes this code and makes it more testable.

/* Smaple ploject */
BROKER SCHEMA COBOLApplication

CREATE FUNCTION OrderList2PurchaseData_Compute_ProcessLogic(IN inputReference REFERENCE, IN outputReference REFERENCE, IN environmentReference REFERENCE) RETURNS BOOLEAN BEGIN

   SET outputReference.DFDL.PurchaseData.InvoiceCount = inputReference.DFDL.OrderList.InvoiceCount;
   SET outputReference.DFDL.PurchaseData.ItemCount = inputReference.DFDL.OrderList.ItemCount;

   DECLARE TotalQuantity INTEGER 0;
   DECLARE I INTEGER 1;
   DECLARE J INTEGER;
   DECLARE InputSaleListRef REFERENCE TO inputReference.DFDL.OrderList.SaleList;
   SET J = CARDINALITY(InputSaleListRef.Invoice[]);
   WHILE I <= J DO
     DECLARE InputSaleListInvoiceRef REFERENCE TO InputSaleListRef.Invoice[I];
     SET outputReference.DFDL.PurchaseData.SaleList.Invoice[I].CustomerInitial[1] = InputSaleListInvoiceRef.InvoiceHeader.CustomerInitial[1];
     DECLARE OutputSaleListInvoiceRef REFERENCE TO outputReference.DFDL.PurchaseData.SaleList.Invoice[I];
     SET OutputSaleListInvoiceRef.CustomerInitial[2] = InputSaleListInvoiceRef.InvoiceHeader.CustomerInitial[2];
     SET OutputSaleListInvoiceRef.Surname = InputSaleListInvoiceRef.InvoiceHeader.Surname;

     DECLARE K INTEGER 1;
     DECLARE L INTEGER;
     SET L = CARDINALITY(InputSaleListInvoiceRef.Items.Item[]);

     WHILE K <= L DO
         DECLARE OutputItem ROW;

         SET OutputItem = THE (
         SELECT Inv.ItemType
         , Inv.ItemCode
         , Inv.Description
         , Inv.Price
         , Inv.Category
         , It.Quantity
         FROM InputSaleListInvoiceRef.Items.Item[K] AS It
         , inputReference.DFDL.OrderList.Inventory.Item[] AS Inv
         WHERE Inv.ItemCode = It.ItemCode
         AND Inv.ItemCode = InputSaleListInvoiceRef.Items.Item[K].ItemCode
         );

       IF OutputItem.ItemType = '1' THEN
         SET OutputSaleListInvoiceRef.Items[K].ItemType = OutputItem.ItemType;
         SET OutputSaleListInvoiceRef.Items[K].Item1.ItemCode = OutputItem.ItemCode;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Description = OutputItem.Description;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Price = OutputItem.Price;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Category = OutputItem.Category;
         SET OutputSaleListInvoiceRef.Items[K].Item1.Quantity = OutputItem.Quantity;
       ELSE
         SET OutputSaleListInvoiceRef.Items[K].ItemType = OutputItem.ItemType;
         SET OutputSaleListInvoiceRef.Items[K].Item2.ItemCode = OutputItem.ItemCode;
         SET OutputSaleListInvoiceRef.Items[K].Item2.Colour = OutputItem.Description;
         SET OutputSaleListInvoiceRef.Items[K].Item2.Price = OutputItem.Price;
         SET OutputSaleListInvoiceRef.Items[K].Item2.Quantity = OutputItem.Quantity;

       END IF;

       SET TotalQuantity = TotalQuantity + OutputItem.Quantity;
       SET K = K + 1;
     END WHILE;

     SET I = I + 1;
     END WHILE;

     --Trailer
     SET outputReference.DFDL.PurchaseData.Trailer.TotalQuantity = TotalQuantity;

     IF EXISTS(inputReference.DFDL.OrderList.PointOfSale1[]) THEN
         SET outputReference.DFDL.PurchaseData.Trailer.PointOfSaleType = '1';
     ELSE
         SET outputReference.DFDL.PurchaseData.Trailer.PointOfSaleType = '2';
     END IF;

     RETURN TRUE;  
  END;

It also rebuilds the Main() so that the more testable code is callable.

CREATE COMPUTE MODULE OrderList2PurchaseData_Compute
     CREATE FUNCTION Main() RETURNS BOOLEAN BEGIN

         CALL CopyMessageHeaders();

         DECLARE inputReference REFERENCE TO InputRoot;
         DECLARE outputReference REFERENCE TO OutputRoot;
         DECLARE environmentReference REFERENCE TO Environment;

         DECLARE logic_res BOOLEAN;
         SET logic_res = OrderList2PurchaseData_Compute_ProcessLogic(inputReference, outputReference, environmentReference);

         RETURN logic_res;
     END;

     CREATE PROCEDURE CopyMessageHeaders() BEGIN
         DECLARE I INTEGER 1;
         DECLARE J INTEGER;
         SET J = CARDINALITY(InputRoot.*[]);
         WHILE I < J DO
             SET OutputRoot.*[I] = InputRoot.*[I];
           SET I = I + 1;
         END WHILE;
         END;

    END MODULE;

It also creates a sample unit test that the developer can copy and extend.

CREATE PROCEDURE OrderList2PurchaseData_Compute_Test() BEGIN

     – CREATE FIELD Environment.Test1;
     – CREATE FIELD Environment.Test1.Input;
     – CREATE FIELD Environment.Test1.Output;
     – DECLARE inputReference REFERENCE TO Environment.Test1.Input;
     – DECLARE outputReference REFERENCE TO Environment.Test1.Output;
     – DECLARE environmentReference REFERENCE TO Environment.Test1.Environment;
     – DECLARE testXML CHARACTER;
     – SET testXML = ‘dataasstring‘;
     – DECLARE xmlBlob BLOB;
     – DECLARE options INTEGER BITOR(FolderBitStream, ValidateContent, ValidateValue);
     – SET xmlBlob = CAST(testXML as BLOB CCSID 1208);
     – CREATE LASTCHILD OF inputReference DOMAIN(‘XMLNSC’) PARSE (xmlBlob, InputProperties.Encoding, InputProperties.CodedCharSetId);

     – DECLARE logic_res BOOLEAN;
     – SET logic_res = OrderList2PurchaseData_Compute_ProcessLogic(inputReference, outputReference, environmentReference);

     – if (outputReference.XMLNSC.Address.Postcode = ’2614′) THEN
     – SET OutputRoot.XMLNSC.Tests.Test1.Result = ‘Passed’;
     – ELSE
     – SET OutputRoot.XMLNSC.Tests.Test1.Result = ‘Failed’;
     – SET OutputRoot.XMLNSC.Tests.Test1.Message = ‘Expected postcode 2614 for Hawker’;
     – END IF;

     – DELETE FIELD Environment.Test1;

END;

Once the developer has some tests they will need to be able to trigger those tests.

The flow uses an MQ message to trigger the tests. The script running the tests then just to check the result message to see if it contains any “failure” messages.

Message flow

We have uploaded a demonstration video which is available from our the tutorials page or is available here.