Skip to Content
Product Information

ABAP Continuous Integration with Azure Devops – ABAP Unit in the Cloud

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 Chairat (Par) Onyaem‘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 Chairat (Par) Onyaem 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.

6 Comments
You must be Logged on to comment or reply to a post.
  • Thanks for this! I was facing the same problem in regards to displaying ABAP Unit results in GitLab merge requests and was about to write my own converter. Now I can just use your XSLT file 🙂

    Regarding links: Either ADT links or WebGUI should be possible. Report RS_AUCV_RUNNER has a parameter to render links in ADT format, so maybe you can take a look there.

  • Thanks For This!!!

    SAP Cloud Platform ABAP Environment is a platform as a service offering for the ABAP developer community running in the Cloud Foundry environment of SAP Cloud Platform. It interacts with the NEO environment especially in the user interface area since tools such as the SAP Web IDE fullstack are located in NEO.

    • Hello,

       

      Yes, and I’m curios to get to try that out, but this blog is stricly based on Netweaver and utilizes the APIs exposed for use by the ADT.

      Regards and happy holidays