Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
Mattias
Active Participant
We're a SAP and Microsoft shop, running SAP CRM and also doing a lot of custom development in .Net on Azure. As part of an ongoing agile journey we will start working in vertical teams covering everything from UI (React), .Net, SAP and testing. One of our main goals is to be able to use the same process and tools as far as possible regardless of what environment the development is done in.

Since we're a Microsoft shop we're already using Azure Devops ( formely known as VSTS) as build server, and Sonarcloud for static code analysis.

In part 1 of this blog series I'm going to describe how we've integrated nightly ABAP Unit test runs into our Devops pipeline.


Background


After reading andreas.gautsch and pacroy's excellent blogs I realized we should also be able to use the APIs exposed to ADT to trigger our unit tests from Devops. We did experiment with the solution that presented by pacroy in https://medium.com/pacroy/continuous-integration-in-abap-3db48fc21028, but concluded that too much data was lost in the Newman script used.

What we however did keep from that test run was the idea that we somehow need to convert the XML result returned from SAP into a format Azure Devops could understand, namely JUnit Result file. After som initial tests we created a small NodeJS script that calls SAP, receives the result and trasforms it using xslt to a format Azure devops can understand.

 

The Script


With no further ado let us dive into the script

  1. First we need to declare all dependencies needed. We're using request-promise to avoid callback-hell and instead rely on promises.
    // Load dependencies
    const xsltProcessor = require('xslt-processor');
    const fs = require("fs");

    const rp = require('request-promise').defaults({ jar: true });

    const path = require("path");
    const { sapProtocol, sapUserName, sapPassword, sapHost, packageToRun, host } = initialize();
    const { xmlRunAbapUnit, xslt } = readXml();​


  2. To call SAP we first need to get a csrt token
    /** Get CSRF Token by calling GET with x-csrf-token: fetch 
    * @returns Promise with the result of the call
    */
    function getCSRFToken() {
    const optionsGetCSRFToken = {
    method: "GET",
    url: host + '/sap/bc/adt/abapunit/testruns',
    simple: false, // Don't handle 405 as an error
    resolveWithFullResponse: true, // Read headers and not only body
    auth: {
    user: sapUserName,
    password: sapPassword
    },
    headers: {
    'X-CSRF-Token': 'fetch'
    }
    };
    return rp(optionsGetCSRFToken);
    }​


  3. Once we have the CSRF-token we can call Netweaver to run our unit tests
    /** Run abap unit tests
    * @param csrf token needed for the call
    * @returns Promise with the result
    */
    function runAbapUnitTest(xCSRFToken) {
    const optionsRunUnitTest = {
    method: 'POST',
    url: host + '/sap/bc/adt/abapunit/testruns',
    auth: {
    user: sapUserName,
    password: sapPassword
    },
    headers: {
    'x-csrf-token': xCSRFToken,
    'Content-Type': "application/xml"
    },
    body: xmlRunAbapUnit
    };
    return rp(optionsRunUnitTest);
    }​


  4. This is where the magic happens, to be able to parse AbapUNIT test results into JUnit results we're relying on xslt-processor to transform the result using an xslt file
    /**
    * Reads XML files needed to run AUnit Tests and transform to JUnit
    * @return xml file with call to run abap unit test, xsl to transform from AUnit Result to JUnit Result
    */function readXml() {
    const xsltData = fs.readFileSync(path.resolve(__dirname, "./xml/aunit2junit.xsl"));
    const xmlRunAbapUnitBuffer = fs.readFileSync(path.resolve(__dirname, "./xml/runAbapUnit.xml"));
    const xslt = xsltProcessor.xmlParse(xsltData.toString()); // xsltString: string of xslt file contents
    const xmlRunAbapUnit = xmlRunAbapUnitBuffer.toString('utf8').replace("{{package}}", packageToRun === undefined ? "ZDOMAIN" : packageToRun); // Default to ZDomain
    return { xmlRunAbapUnit, xslt };
    }​


  5. Finally this is all tied together in a mains method
    /** Runs the abap unit test and converts them to JUnit format
    * 1) Get CSRF Token
    * 2) Call Netweaver Server and get abap unit results
    * 3) Transform result and save to output.xml
    **/
    function main() {
    csrfTokenPromise = getCSRFToken();

    var runAbapUnitTestPromise = csrfTokenPromise.then(function (response) {
    var csrfToken = response.headers['x-csrf-token'];
    return runAbapUnitTest(csrfToken);
    }
    ).catch(function (err) {
    console.error(JSON.stringify(err));
    }
    );

    runAbapUnitTestPromise.then(function (parsedBody) {
    const xml = xsltProcessor.xmlParse(parsedBody); // xsltString: string of xslt file contents
    const outXmlString = xsltProcessor.xsltProcess(xml, xslt); // outXmlString: output xml string.
    fs.writeFileSync("output.xml", outXmlString)
    }).catch(function (err) {
    console.error(JSON.stringify(err));
    });
    }



 

The XSLT and XML


Luckily the ABAPUnit- and JUnit formats are pretty similar, so I could more or less do a 1-1 mapping (since xslt is magic, this was a good thing). If you have any idea how to make this better, please create a Pull Request on GitHub!
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:aunit="http://www.sap.com/adt/aunit"
xmlns:adtcore="http://www.sap.com/adt/core">
<xsl:template match="/">
<testsuites>
<xsl:attribute name="tests">
<xsl:value-of
select="count(aunit:runResult/program/testClasses/testClass/testMethods/testMethod)" />
</xsl:attribute>
<xsl:attribute name="failures">
<xsl:value-of
select="count(aunit:runResult/program/testClasses/testClass/testMethods/testMethod/alerts/alert)" />
</xsl:attribute>
<xsl:for-each select="aunit:runResult/program">
<xsl:variable name="object" select="@adtcore:name" />
<testsuite>
<xsl:attribute name="name">
<xsl:value-of select="@adtcore:packageName" />
</xsl:attribute>
<xsl:attribute name="tests">
<xsl:value-of
select="count(testClasses/testClass/testMethods/testMethod)" />
</xsl:attribute>
<xsl:attribute name="failures">
<xsl:value-of
select="count(testClasses/testClass/testMethods/testMethod/alerts/alert)" />
</xsl:attribute>
<xsl:attribute name="package">
<xsl:value-of select="@adtcore:uri" />
</xsl:attribute>

<xsl:for-each
select="testClasses/testClass/testMethods/testMethod">
<testcase>
<xsl:attribute name="name">
<xsl:value-of select="@adtcore:packageName" /> - <xsl:value-of select="$object" /> - <xsl:value-of select="@adtcore:name" />
</xsl:attribute>
<xsl:attribute name="classname">
<xsl:value-of select="@adtcore:uri" />
</xsl:attribute>
<xsl:attribute name="time">
<xsl:value-of select="@executionTime" />
</xsl:attribute>
<xsl:for-each select="alerts/alert">
<failure>
<xsl:attribute name="message">
<xsl:value-of select="title" />
</xsl:attribute>
<xsl:attribute name="type">
<xsl:value-of select="@severity" />
</xsl:attribute>
<xsl:for-each select="details/detail">
<xsl:value-of select="@text" />
<xsl:value-of select="'
'" />
<xsl:for-each select="details/detail">
<xsl:value-of select="@text" />
<xsl:value-of select="'
'" />
</xsl:for-each>
</xsl:for-each>
</failure>
</xsl:for-each>
</testcase>
</xsl:for-each>

</testsuite>
</xsl:for-each>
</testsuites>
</xsl:template>

</xsl:stylesheet>

And finally we have the XML body sent into Netweaver to trigger the ABAP Unit runs.
<?xml version='1.0' encoding='UTF-8'?>
<aunit:runConfiguration xmlns:aunit='http://www.sap.com/adt/aunit'>
<external>
<coverage active='false'/>
</external>
<adtcore:objectSets xmlns:adtcore='http://www.sap.com/adt/core'>
<objectSet kind='inclusive'>
<adtcore:objectReferences>
<adtcore:objectReference adtcore:uri='/sap/bc/adt/vit/wb/object_type/devck/object_name/{{package}}'/>
</adtcore:objectReferences>
</objectSet>
</adtcore:objectSets>
</aunit:runConfiguration>

The full code


The full project is available on GitHub at https://github.com/trr-official/abapunit2junit. Please have a look and propose changes, it is by no means perfect!

The Result


The result is that we run nightly testruns in our Devop environment (Azure DevOps) and can see that the number of failed unit tests does not keep dropping 🙂 We can also drill down into individual errors, although I think the Abap Test Cockpit is more suited to this with direct navigation to the failing objects and so on.







The future


On my wishlist is the possibility to open failing objects directly from Devops using ADT Links, but I have not figured out how to do this yet.

We're also working on integration AbapGIT in our workflow and using SonarCloud for code analytics, this will be subject for another post.
19 Comments