ABAP Unit Runner auf BTP: Automatisierte Tests in SAP BTP ABAP Environment

Kategorie
BTP
Veröffentlicht
Autor
Johannes

Automatisierte Unit Tests sind ein zentraler Bestandteil moderner Softwareentwicklung. In SAP BTP ABAP Environment kannst du ABAP Unit Tests über APIs ausführen und in CI/CD-Pipelines integrieren. Dieser Artikel zeigt dir, wie du den ABAP Unit Runner einrichtest und nutzt.

Warum automatisierte Tests auf BTP?

In einer Cloud-Umgebung ist kontinuierliches Testen unverzichtbar:

AspektManuellAutomatisiert
AusführungBei Bedarf im ADTBei jedem Commit
KonsistenzVariiertImmer gleich
GeschwindigkeitLangsamSchnell
CI/CDNicht möglichVollständig integriert
NachvollziehbarkeitBegrenztKomplett protokolliert
┌────────────────────────────────────────────────────────────────┐
│ CI/CD Pipeline mit ABAP Unit Runner │
├────────────────────────────────────────────────────────────────┤
│ │
│ Git Push ──▶ Build ──▶ ABAP Unit ──▶ ATC ──▶ Deploy │
│ Runner API API │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ BTP │ │
│ │ ABAP │ │
│ │ System │ │
│ └──────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘

Voraussetzungen

Bevor du den ABAP Unit Runner nutzen kannst, benötigst du:

  1. SAP BTP ABAP Environment (Steampunk-Instanz)
  2. Communication User mit entsprechenden Berechtigungen
  3. Communication Arrangement für ADT-Services
  4. Service Key für API-Zugriff

Communication Arrangement einrichten

Das Communication Arrangement ermöglicht den externen Zugriff auf die ADT-APIs:

┌────────────────────────────────────────────────────────────────┐
│ Communication Arrangement: SAP_COM_0763 │
├────────────────────────────────────────────────────────────────┤
│ │
│ Scenario: SAP_COM_0763 (ABAP Development Tools) │
│ │
│ Communication System: Z_CICD_SYSTEM │
│ ├─ Host: github.actions.runner │
│ └─ Business System ID: CICD_RUNNER │
│ │
│ Communication User: CICD_USER │
│ ├─ Authentication: Basic / OAuth 2.0 │
│ └─ Password/Client Secret: ******** │
│ │
│ Inbound Services: │
│ ☑ ADT Core Services │
│ ☑ ADT ABAP Unit │
│ ☑ ADT ATC │
│ │
└────────────────────────────────────────────────────────────────┘

Schritt-für-Schritt:

  1. Fiori Launchpad öffnen → Communication Arrangements
  2. Neu → Scenario SAP_COM_0763 wählen
  3. Communication System anlegen oder vorhandenes wählen
  4. Communication User mit Passwort konfigurieren
  5. Inbound-Services aktivieren

ADT REST API für Unit Tests

Die ADT REST API ermöglicht die programmatische Ausführung von ABAP Unit Tests.

API-Endpunkt

POST https://<system-url>/sap/bc/adt/abapunit/testruns

Request-Format

Der Request verwendet ein XML-Format zur Konfiguration der Testausführung:

<?xml version="1.0" encoding="UTF-8"?>
<aunit:runConfiguration xmlns:aunit="http://www.sap.com/adt/aunit">
<external>
<coverage active="true" branchCoverage="true"/>
</external>
<options>
<uriType value="semantic"/>
<testDeterminationStrategy sameProgram="true" assignedTests="false"/>
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="true"/>
<critical active="true"/>
</testRiskCoverage>
<durationCoverage short="true" medium="true" long="true"/>
<withNavigationUri enabled="true"/>
</options>
<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/Z_FLIGHT_BOOKING"/>
</adtcore:objectReferences>
</objectSet>
</adtcore:objectSets>
</aunit:runConfiguration>

Konfigurationsoptionen

OptionWerteBeschreibung
coveragetrue/falseCode Coverage aktivieren
branchCoveragetrue/falseBranch Coverage zusätzlich
harmlesstrue/falseTests mit Risk Level HARMLESS
dangeroustrue/falseTests mit Risk Level DANGEROUS
criticaltrue/falseTests mit Risk Level CRITICAL
shorttrue/falseTests mit Duration SHORT
mediumtrue/falseTests mit Duration MEDIUM
longtrue/falseTests mit Duration LONG

cURL-Beispiel

Terminal window
# ABAP Unit Tests für ein Paket ausführen
curl -X POST \
"https://my-system.abap.eu10.hana.ondemand.com/sap/bc/adt/abapunit/testruns" \
-u "CICD_USER:password" \
-H "Content-Type: application/vnd.sap.adt.abapunit.testruns.config.v4+xml" \
-H "Accept: application/vnd.sap.adt.abapunit.testruns.result.v1+xml" \
-d @aunit_config.xml \
-o aunit_results.xml

Testergebnisse auswerten

Response-Format

Die API liefert die Ergebnisse im XML-Format:

<?xml version="1.0" encoding="UTF-8"?>
<aunit:runResult xmlns:aunit="http://www.sap.com/adt/aunit">
<program adtcore:name="ZCL_FLIGHT_BOOKING" adtcore:type="CLAS">
<testClasses>
<testClass adtcore:name="LTC_BOOKING" adtcore:uri="/sap/bc/adt/oo/classes/zcl_flight_booking/source/testclasses#start=10,0">
<testMethods>
<testMethod adtcore:name="TEST_VALID_BOOKING"
executionTime="00:00:00.015"
uriType="semantic">
<alerts/>
</testMethod>
<testMethod adtcore:name="TEST_INVALID_DATE"
executionTime="00:00:00.008"
uriType="semantic">
<alerts/>
</testMethod>
<testMethod adtcore:name="TEST_MISSING_PASSENGER"
executionTime="00:00:00.012"
uriType="semantic">
<alerts>
<alert kind="failure" severity="critical">
<title>Assertion failed</title>
<details>Expected: 'ERROR', Actual: 'OK'</details>
<stack>
<stackEntry uri="/sap/bc/adt/oo/classes/zcl_flight_booking/source/testclasses#start=45,4"/>
</stack>
</alert>
</alerts>
</testMethod>
</testMethods>
</testClass>
</testClasses>
</program>
<coverage>
<statementCoverage percentage="78.5"/>
<branchCoverage percentage="65.2"/>
</coverage>
</aunit:runResult>

Ergebnis-Parsing in Bash

parse_aunit_results.sh
#!/bin/bash
RESULTS_FILE="aunit_results.xml"
# Gesamtzahl Tests
TOTAL_TESTS=$(grep -c '<testMethod' "$RESULTS_FILE" || echo "0")
# Erfolgreiche Tests (keine alerts)
PASSED_TESTS=$(grep -c '<alerts/>' "$RESULTS_FILE" || echo "0")
# Fehlgeschlagene Tests
FAILED_TESTS=$((TOTAL_TESTS - PASSED_TESTS))
# Coverage extrahieren
STATEMENT_COVERAGE=$(grep -oP 'statementCoverage percentage="\K[^"]+' "$RESULTS_FILE" || echo "0")
BRANCH_COVERAGE=$(grep -oP 'branchCoverage percentage="\K[^"]+' "$RESULTS_FILE" || echo "0")
echo "================================"
echo "ABAP Unit Test Report"
echo "================================"
echo "Total Tests: $TOTAL_TESTS"
echo "Passed: $PASSED_TESTS"
echo "Failed: $FAILED_TESTS"
echo ""
echo "Statement Coverage: ${STATEMENT_COVERAGE}%"
echo "Branch Coverage: ${BRANCH_COVERAGE}%"
echo "================================"
# Exit Code für CI/CD
if [ "$FAILED_TESTS" -gt 0 ]; then
echo "::error::$FAILED_TESTS test(s) failed!"
exit 1
fi
# Coverage-Threshold prüfen
THRESHOLD=70
if (( $(echo "$STATEMENT_COVERAGE < $THRESHOLD" | bc -l) )); then
echo "::warning::Coverage ${STATEMENT_COVERAGE}% is below threshold ${THRESHOLD}%"
fi
exit 0

Ergebnis-Parsing mit Python

parse_aunit_results.py
import xml.etree.ElementTree as ET
import sys
import json
def parse_aunit_results(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
ns = {
'aunit': 'http://www.sap.com/adt/aunit',
'adtcore': 'http://www.sap.com/adt/core'
}
results = {
'total': 0,
'passed': 0,
'failed': 0,
'errors': [],
'coverage': {}
}
# Tests zählen
for test_method in root.findall('.//aunit:testMethod', ns):
results['total'] += 1
alerts = test_method.find('aunit:alerts', ns)
if alerts is not None and len(alerts) == 0:
results['passed'] += 1
else:
results['failed'] += 1
# Fehlerdetails sammeln
for alert in alerts.findall('aunit:alert', ns):
results['errors'].append({
'test': test_method.get('{http://www.sap.com/adt/core}name'),
'kind': alert.get('kind'),
'title': alert.findtext('aunit:title', '', ns),
'details': alert.findtext('aunit:details', '', ns)
})
# Coverage extrahieren
coverage = root.find('.//aunit:coverage', ns)
if coverage is not None:
stmt = coverage.find('aunit:statementCoverage', ns)
branch = coverage.find('aunit:branchCoverage', ns)
if stmt is not None:
results['coverage']['statement'] = float(stmt.get('percentage', 0))
if branch is not None:
results['coverage']['branch'] = float(branch.get('percentage', 0))
return results
if __name__ == '__main__':
results = parse_aunit_results(sys.argv[1])
print(json.dumps(results, indent=2))
# Exit mit Fehlercode wenn Tests fehlgeschlagen
sys.exit(1 if results['failed'] > 0 else 0)

CI/CD-Integration mit GitHub Actions

Komplette GitHub Actions Workflow

.github/workflows/abap-unit-tests.yml
name: ABAP Unit Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
ABAP_PACKAGE: Z_FLIGHT_BOOKING
COVERAGE_THRESHOLD: 70
jobs:
unit-tests:
name: Run ABAP Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Create Test Configuration
run: |
cat > aunit_config.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<aunit:runConfiguration xmlns:aunit="http://www.sap.com/adt/aunit">
<external>
<coverage active="true" branchCoverage="true"/>
</external>
<options>
<uriType value="semantic"/>
<testDeterminationStrategy sameProgram="true" assignedTests="false"/>
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="true"/>
<critical active="true"/>
</testRiskCoverage>
<durationCoverage short="true" medium="true" long="true"/>
<withNavigationUri enabled="true"/>
</options>
<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/${{ env.ABAP_PACKAGE }}"/>
</adtcore:objectReferences>
</objectSet>
</adtcore:objectSets>
</aunit:runConfiguration>
EOF
- name: Run ABAP Unit Tests
id: run-tests
run: |
HTTP_CODE=$(curl -s -w "%{http_code}" \
-X POST \
"${{ secrets.ABAP_ENDPOINT }}/sap/bc/adt/abapunit/testruns" \
-u "${{ secrets.ABAP_USER }}:${{ secrets.ABAP_PASSWORD }}" \
-H "Content-Type: application/vnd.sap.adt.abapunit.testruns.config.v4+xml" \
-H "Accept: application/vnd.sap.adt.abapunit.testruns.result.v1+xml" \
-d @aunit_config.xml \
-o aunit_results.xml)
if [ "$HTTP_CODE" != "200" ]; then
echo "::error::API call failed with HTTP $HTTP_CODE"
cat aunit_results.xml
exit 1
fi
echo "API call successful"
- name: Parse Test Results
id: parse
run: |
python << 'PYTHON_SCRIPT'
import xml.etree.ElementTree as ET
import os
tree = ET.parse('aunit_results.xml')
root = tree.getroot()
ns = {'aunit': 'http://www.sap.com/adt/aunit', 'adtcore': 'http://www.sap.com/adt/core'}
total = len(root.findall('.//aunit:testMethod', ns))
passed = len([m for m in root.findall('.//aunit:testMethod', ns)
if len(m.find('aunit:alerts', ns)) == 0])
failed = total - passed
# Coverage
coverage = root.find('.//aunit:coverage', ns)
stmt_cov = 0
if coverage is not None:
stmt = coverage.find('aunit:statementCoverage', ns)
if stmt is not None:
stmt_cov = float(stmt.get('percentage', 0))
# GitHub Output
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f"total={total}\n")
f.write(f"passed={passed}\n")
f.write(f"failed={failed}\n")
f.write(f"coverage={stmt_cov}\n")
print(f"Total: {total}, Passed: {passed}, Failed: {failed}")
print(f"Statement Coverage: {stmt_cov}%")
if failed > 0:
print("::error::Some tests failed!")
exit(1)
PYTHON_SCRIPT
- name: Check Coverage Threshold
run: |
COVERAGE=${{ steps.parse.outputs.coverage }}
THRESHOLD=${{ env.COVERAGE_THRESHOLD }}
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
echo "::warning::Coverage ${COVERAGE}% is below threshold ${THRESHOLD}%"
fi
- name: Upload Test Results
uses: actions/upload-artifact@v4
with:
name: aunit-results
path: aunit_results.xml
- name: Create Test Summary
if: always()
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
## ABAP Unit Test Results
| Metric | Value |
|--------|-------|
| Total Tests | ${{ steps.parse.outputs.total }} |
| Passed | ${{ steps.parse.outputs.passed }} |
| Failed | ${{ steps.parse.outputs.failed }} |
| Coverage | ${{ steps.parse.outputs.coverage }}% |
EOF

Reusable Workflow für mehrere Pakete

.github/workflows/abap-unit-reusable.yml
name: ABAP Unit Tests (Reusable)
on:
workflow_call:
inputs:
package:
required: true
type: string
description: 'ABAP Package to test'
coverage_threshold:
required: false
type: number
default: 70
description: 'Minimum coverage percentage'
include_dangerous:
required: false
type: boolean
default: true
description: 'Include RISK LEVEL DANGEROUS tests'
secrets:
ABAP_ENDPOINT:
required: true
ABAP_USER:
required: true
ABAP_PASSWORD:
required: true
outputs:
total:
description: 'Total number of tests'
value: ${{ jobs.test.outputs.total }}
passed:
description: 'Number of passed tests'
value: ${{ jobs.test.outputs.passed }}
coverage:
description: 'Statement coverage percentage'
value: ${{ jobs.test.outputs.coverage }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
total: ${{ steps.parse.outputs.total }}
passed: ${{ steps.parse.outputs.passed }}
coverage: ${{ steps.parse.outputs.coverage }}
steps:
- name: Run ABAP Unit Tests for ${{ inputs.package }}
run: |
curl -s -X POST \
"${{ secrets.ABAP_ENDPOINT }}/sap/bc/adt/abapunit/testruns" \
-u "${{ secrets.ABAP_USER }}:${{ secrets.ABAP_PASSWORD }}" \
-H "Content-Type: application/vnd.sap.adt.abapunit.testruns.config.v4+xml" \
-H "Accept: application/vnd.sap.adt.abapunit.testruns.result.v1+xml" \
-d '<aunit:runConfiguration xmlns:aunit="http://www.sap.com/adt/aunit">
<external><coverage active="true"/></external>
<options>
<uriType value="semantic"/>
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="${{ inputs.include_dangerous }}"/>
</testRiskCoverage>
<durationCoverage short="true" medium="true" long="true"/>
</options>
<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/${{ inputs.package }}"/>
</adtcore:objectReferences>
</objectSet>
</adtcore:objectSets>
</aunit:runConfiguration>' \
-o results.xml
- name: Parse Results
id: parse
run: |
# Parsing-Logik hier (wie oben)
echo "total=10" >> $GITHUB_OUTPUT
echo "passed=9" >> $GITHUB_OUTPUT
echo "coverage=85.5" >> $GITHUB_OUTPUT

Nutzung des Reusable Workflows

.github/workflows/main.yml
name: CI Pipeline
on:
push:
branches: [ main ]
jobs:
test-booking:
uses: ./.github/workflows/abap-unit-reusable.yml
with:
package: Z_FLIGHT_BOOKING
coverage_threshold: 80
secrets: inherit
test-customer:
uses: ./.github/workflows/abap-unit-reusable.yml
with:
package: Z_CUSTOMER
coverage_threshold: 75
secrets: inherit
summary:
needs: [test-booking, test-customer]
runs-on: ubuntu-latest
steps:
- name: Test Summary
run: |
echo "Booking Tests: ${{ needs.test-booking.outputs.passed }}/${{ needs.test-booking.outputs.total }}"
echo "Customer Tests: ${{ needs.test-customer.outputs.passed }}/${{ needs.test-customer.outputs.total }}"

Spezifische Tests ausführen

Tests für einzelne Klassen

Terminal window
# Nur Tests für eine bestimmte Klasse
curl -X POST \
"$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \
-u "$USER:$PASSWORD" \
-H "Content-Type: application/vnd.sap.adt.abapunit.testruns.config.v4+xml" \
-H "Accept: application/vnd.sap.adt.abapunit.testruns.result.v1+xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<aunit:runConfiguration xmlns:aunit="http://www.sap.com/adt/aunit">
<options><uriType value="semantic"/></options>
<adtcore:objectSets xmlns:adtcore="http://www.sap.com/adt/core">
<objectSet kind="inclusive">
<adtcore:objectReferences>
<adtcore:objectReference
adtcore:uri="/sap/bc/adt/oo/classes/zcl_flight_booking"/>
</adtcore:objectReferences>
</objectSet>
</adtcore:objectSets>
</aunit:runConfiguration>'

Tests mit bestimmtem Risk Level

<!-- Nur HARMLESS Tests (für schnelles Feedback) -->
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="false"/>
<critical active="false"/>
</testRiskCoverage>
<!-- Alle Tests inklusive DANGEROUS (Nightly Build) -->
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="true"/>
<critical active="true"/>
</testRiskCoverage>

Tests mit bestimmter Duration

<!-- Nur SHORT Tests (für Pull Requests) -->
<durationCoverage short="true" medium="false" long="false"/>
<!-- Alle Tests (für Nightly Build) -->
<durationCoverage short="true" medium="true" long="true"/>

Code Coverage analysieren

Coverage-Details abrufen

Die Coverage-Informationen sind im Response enthalten:

<coverage>
<statementCoverage percentage="78.5"/>
<branchCoverage percentage="65.2"/>
<procedureCoverage percentage="92.0"/>
</coverage>

Coverage-Bericht generieren

generate_coverage_report.py
import xml.etree.ElementTree as ET
import sys
def generate_coverage_report(xml_file, threshold=70):
tree = ET.parse(xml_file)
root = tree.getroot()
ns = {'aunit': 'http://www.sap.com/adt/aunit'}
coverage = root.find('.//aunit:coverage', ns)
if coverage is None:
print("Keine Coverage-Daten gefunden")
return 1
metrics = {}
for child in coverage:
tag = child.tag.replace('{http://www.sap.com/adt/aunit}', '')
metrics[tag] = float(child.get('percentage', 0))
print("=" * 50)
print("CODE COVERAGE REPORT")
print("=" * 50)
for metric, value in metrics.items():
status = "" if value >= threshold else ""
print(f"{status} {metric}: {value:.1f}%")
print("=" * 50)
print(f"Threshold: {threshold}%")
# Badge für README generieren
stmt_cov = metrics.get('statementCoverage', 0)
if stmt_cov >= 80:
color = 'brightgreen'
elif stmt_cov >= 60:
color = 'yellow'
else:
color = 'red'
badge_url = f"https://img.shields.io/badge/coverage-{stmt_cov:.0f}%25-{color}"
print(f"\nBadge URL: {badge_url}")
return 0 if stmt_cov >= threshold else 1
if __name__ == '__main__':
threshold = int(sys.argv[2]) if len(sys.argv) > 2 else 70
sys.exit(generate_coverage_report(sys.argv[1], threshold))

Troubleshooting

Häufige Fehler

FehlerUrsacheLösung
401 UnauthorizedFalsche CredentialsUser/Passwort prüfen
403 ForbiddenFehlende BerechtigungenCommunication Arrangement prüfen
404 Not FoundPaket existiert nichtPaketname prüfen
500 Internal ErrorSystemfehlerSystem-Logs prüfen
TimeoutTests zu langeDuration-Filter nutzen

Debugging-Tipps

Terminal window
# Verbose Output für Debugging
curl -v -X POST \
"$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \
-u "$USER:$PASSWORD" \
-H "Content-Type: application/vnd.sap.adt.abapunit.testruns.config.v4+xml" \
-H "Accept: application/vnd.sap.adt.abapunit.testruns.result.v1+xml" \
-d @config.xml 2>&1 | tee debug.log

Timeout erhöhen

Terminal window
# Timeout auf 5 Minuten setzen
curl --max-time 300 \
-X POST \
"$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \
...

Best Practices

AspektEmpfehlung
TeststrukturTests nach Risk Level und Duration kategorisieren
CI/CDSHORT/HARMLESS für PRs, alle für Nightly
CoverageMinimum 70%, Ziel 80%+
FehlerbehandlungDetaillierte Fehlerausgabe in Pipeline
CachingService Keys sicher speichern (Secrets)
ParallelisierungMehrere Pakete parallel testen
ArtefakteErgebnisse als Artefakte speichern
BenachrichtigungSlack/Teams bei Fehlern benachrichtigen

Weitere Ressourcen