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
- 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();
- 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); }
- 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); }
- 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 }; }
- 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.
Great works! Will try this out.
PS: My Golang learning does not progress much 🙂
Thanks for the issues, I hope you can continue testing now
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.
It's my second XSLT ever, so please let us improve it 🙂
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
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.
Hi Mattias,
We are trying to setup CI/CD pipeline for both ABAP and SAP UI5 developments.Will this work for SAP UI5 code also?
No, this specifically calls a backend function that triggers Abap unit. We have a pipeline setup with Selenium for UI testing, but we're not using UI5. I guess you should be able to setup OPA in a pipeline.
Hello Hitesh,
Were you able to complete the CI/CD process for ABAP and did you tried this as well now ?
Hello Mattias,
We are also trying to implement the whole thing now, and in between I got your post here, so I just wanted to know that are you having any basic documentation which explains the early steps, to implement CI/CD with ABAP on azure or any helpful links which can help us as a fresher in SAP.
We actually know the concept but don't know from where to start and what would be the requirements we are going to need.
Thanks in advance
I think this blog is a good start for the automated tests, together with reading up on abapgit. I've also created a Azure Devops Task that runs abap unit tests, but it still need to be cleaned up before releasing it into the wild.
Also you could include abaplint into your build flow, but we have not yet done that.
Hello Mattias,
We are trying to setup an abap CI/CD pipeline and following your work, it is helping a lot.
Our SAP system is inside company's network and one more issue, that the environment variables are needed to be declared, but there is still some error while doing the test.
Also have a look at SAP-Cli by Jakub Filak, it's a CMD-line tool to work with Abap, if I was to start over I would probably go with this
https://github.com/jfilak/sapcli