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.

12 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.

  • Nice! I created something similar in Python https://github.com/jfilak/sapcli – I am slowly adding more ADT wrappers like releasing transports or editing objects.

    • Nice, how do you present the result of the abap unit run? I tried running it but got stuck on some errors, I’ve filed an issue on Github.

       

      If you just show the aunit-xml-format I guess you could pipe it through my xslt to create the junit-format that most CI tools need. Anyway it would be nice to get rid of the node dependency I have, it slows down our pipeline.

       

      • Sorry about the troubles with starting the tool. I have added a python starter script which should solve the troubles.

        At this moment, the output of sapcli aunit run {package,class,program} $NAME is aunit-xml-format but I want to add a simple conversion to a text format good for console nerds like me and xunit format for Jenkins.

        Yeah, dependencies! I try to avoid them, hence the only requirement I have is python and python-requests – no xml processing, no library for serialization, …

    • I was also working on releasing requests, but got stuck and left it.

      We have a flow where we approve PRs, and would like to release the corresponding request once the PR is approved), but I had two problems

      1. I couldn’t find an actual ADT call that releases tasks, instead ADT opens SAP GUI to do it
      2. In my use case I could not find a good “key” between my git-branch and which release should be closed. Even if I’ve made $abapgit rename the branch according to the transport, I cannot be sure that all objects in the branch belongs to a transport and vice vers. But this I guess is specific to my use case.

      If you manage to release tasks that would be very useful!

       

      • Yep, I regularly release my task from command line. I was born in 80s and  I need plain text format, so most of the time I work in console. Now, when I have some codebase to play with, I guess I will add some handy features like “release recursive” which would release all tasks of a transport.

        • Comparing objects on a branch and objects in a transport can be done:

          git diff --name-only master HEAD

          ./sapcli cts list transport $NUMBER
          Assuming you are on the pull request branch and $NUMBER holds transport number.

  • First of all Mattias , this is really great work. But I have some questions .

    Can I trigger such script from an user story level in Azure DevOps ? Also , can I pass some input parameter  to it ?

    So , as an example, I customize the user story screen and add a new field to capture the SAP transport request number. Then once the build status is complete, I need your scripts to get triggered with the transport request number so that it runs the ABAP Unit test on SAP side only for the objects under the transport request .

     

    Is it possible ? Not sure whether the Web hook option under Service hook create subscription can take care of it since I don’t find any option to pass the parameter there

     

    • Hi,

       

      Thank you for your questions. I’m not sure how easy it is to pass parameters in Azure Devops, what we’ve been experimenting with is automatically create a new branch with the transport number as name from Abapgit (if you use the feature “Transport to Branch” it will do that from a couple of months ago.

      Another problem is that the API I’m using for running the unit tests accepts development objects as parameters, not transport requests, so somehow you need to find all objects in that transport. That should be doable if you use one branch per transport, but I have not tried it. For now we’re happy with the nightly runs, and for the transports vi manually run tests via ATC.

      As for the script, if you have basic JS knowledge it’s easy to customize, very little magic going on.