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:
| Aspekt | Manuell | Automatisiert |
|---|---|---|
| Ausführung | Bei Bedarf im ADT | Bei jedem Commit |
| Konsistenz | Variiert | Immer gleich |
| Geschwindigkeit | Langsam | Schnell |
| CI/CD | Nicht möglich | Vollständig integriert |
| Nachvollziehbarkeit | Begrenzt | Komplett 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:
- SAP BTP ABAP Environment (Steampunk-Instanz)
- Communication User mit entsprechenden Berechtigungen
- Communication Arrangement für ADT-Services
- 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:
- Fiori Launchpad öffnen → Communication Arrangements
- Neu → Scenario SAP_COM_0763 wählen
- Communication System anlegen oder vorhandenes wählen
- Communication User mit Passwort konfigurieren
- 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/testrunsRequest-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
| Option | Werte | Beschreibung |
|---|---|---|
| coverage | true/false | Code Coverage aktivieren |
| branchCoverage | true/false | Branch Coverage zusätzlich |
| harmless | true/false | Tests mit Risk Level HARMLESS |
| dangerous | true/false | Tests mit Risk Level DANGEROUS |
| critical | true/false | Tests mit Risk Level CRITICAL |
| short | true/false | Tests mit Duration SHORT |
| medium | true/false | Tests mit Duration MEDIUM |
| long | true/false | Tests mit Duration LONG |
cURL-Beispiel
# ABAP Unit Tests für ein Paket ausführencurl -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.xmlTestergebnisse 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
#!/bin/bashRESULTS_FILE="aunit_results.xml"
# Gesamtzahl TestsTOTAL_TESTS=$(grep -c '<testMethod' "$RESULTS_FILE" || echo "0")
# Erfolgreiche Tests (keine alerts)PASSED_TESTS=$(grep -c '<alerts/>' "$RESULTS_FILE" || echo "0")
# Fehlgeschlagene TestsFAILED_TESTS=$((TOTAL_TESTS - PASSED_TESTS))
# Coverage extrahierenSTATEMENT_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/CDif [ "$FAILED_TESTS" -gt 0 ]; then echo "::error::$FAILED_TESTS test(s) failed!" exit 1fi
# Coverage-Threshold prüfenTHRESHOLD=70if (( $(echo "$STATEMENT_COVERAGE < $THRESHOLD" | bc -l) )); then echo "::warning::Coverage ${STATEMENT_COVERAGE}% is below threshold ${THRESHOLD}%"fi
exit 0Ergebnis-Parsing mit Python
import xml.etree.ElementTree as ETimport sysimport 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
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 }}% |
EOFReusable Workflow für mehrere Pakete
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_OUTPUTNutzung des Reusable Workflows
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
# Nur Tests für eine bestimmte Klassecurl -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
import xml.etree.ElementTree as ETimport 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
| Fehler | Ursache | Lösung |
|---|---|---|
| 401 Unauthorized | Falsche Credentials | User/Passwort prüfen |
| 403 Forbidden | Fehlende Berechtigungen | Communication Arrangement prüfen |
| 404 Not Found | Paket existiert nicht | Paketname prüfen |
| 500 Internal Error | Systemfehler | System-Logs prüfen |
| Timeout | Tests zu lange | Duration-Filter nutzen |
Debugging-Tipps
# Verbose Output für Debuggingcurl -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.logTimeout erhöhen
# Timeout auf 5 Minuten setzencurl --max-time 300 \ -X POST \ "$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \ ...Best Practices
| Aspekt | Empfehlung |
|---|---|
| Teststruktur | Tests nach Risk Level und Duration kategorisieren |
| CI/CD | SHORT/HARMLESS für PRs, alle für Nightly |
| Coverage | Minimum 70%, Ziel 80%+ |
| Fehlerbehandlung | Detaillierte Fehlerausgabe in Pipeline |
| Caching | Service Keys sicher speichern (Secrets) |
| Parallelisierung | Mehrere Pakete parallel testen |
| Artefakte | Ergebnisse als Artefakte speichern |
| Benachrichtigung | Slack/Teams bei Fehlern benachrichtigen |
Weitere Ressourcen
- Unit Tests für RAP Services - RAP-spezifisches Testing
- CI/CD mit ABAP Cloud - Vollständige Pipeline-Beispiele
- ABAP Unit Testing in ABAP Cloud - Grundlagen Unit Testing
- ATC Automatisierung auf BTP - ATC in CI/CD integrieren