Les tests unitaires automatisés sont un élément central du développement logiciel moderne. Dans SAP BTP ABAP Environment, vous pouvez exécuter des tests ABAP Unit via des APIs et les intégrer dans des pipelines CI/CD. Cet article vous montre comment configurer et utiliser l’ABAP Unit Runner.
Pourquoi des tests automatisés sur BTP ?
Dans un environnement cloud, les tests continus sont indispensables :
| Aspect | Manuel | Automatisé |
|---|---|---|
| Exécution | À la demande dans ADT | À chaque commit |
| Cohérence | Variable | Toujours identique |
| Rapidité | Lent | Rapide |
| CI/CD | Impossible | Intégration complète |
| Traçabilité | Limitée | Entièrement protocollée |
┌────────────────────────────────────────────────────────────────┐│ Pipeline CI/CD avec ABAP Unit Runner │├────────────────────────────────────────────────────────────────┤│ ││ Git Push ──▶ Build ──▶ ABAP Unit ──▶ ATC ──▶ Deploy ││ Runner API API ││ │ ││ ▼ ││ ┌──────────┐ ││ │ BTP │ ││ │ ABAP │ ││ │ System │ ││ └──────────┘ ││ │└────────────────────────────────────────────────────────────────┘Prérequis
Avant de pouvoir utiliser l’ABAP Unit Runner, vous avez besoin de :
- SAP BTP ABAP Environment (instance Steampunk)
- Communication User avec les autorisations correspondantes
- Communication Arrangement pour les services ADT
- Service Key pour l’accès API
Configuration du Communication Arrangement
Le Communication Arrangement permet l’accès externe aux APIs ADT :
┌────────────────────────────────────────────────────────────────┐│ 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 ││ │└────────────────────────────────────────────────────────────────┘Étape par étape :
- Ouvrir Fiori Launchpad → Communication Arrangements
- Nouveau → Choisir le scénario SAP_COM_0763
- Créer ou sélectionner un Communication System existant
- Configurer Communication User avec mot de passe
- Activer les Inbound Services
API REST ADT pour les tests unitaires
L’API REST ADT permet l’exécution programmatique de tests ABAP Unit.
Point de terminaison API
POST https://<system-url>/sap/bc/adt/abapunit/testrunsFormat de requête
La requête utilise un format XML pour configurer l’exécution des tests :
<?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>Options de configuration
| Option | Valeurs | Description |
|---|---|---|
| coverage | true/false | Activer la couverture de code |
| branchCoverage | true/false | Couverture de branche en plus |
| harmless | true/false | Tests avec Risk Level HARMLESS |
| dangerous | true/false | Tests avec Risk Level DANGEROUS |
| critical | true/false | Tests avec Risk Level CRITICAL |
| short | true/false | Tests avec Duration SHORT |
| medium | true/false | Tests avec Duration MEDIUM |
| long | true/false | Tests avec Duration LONG |
Exemple cURL
# Exécuter les tests ABAP Unit pour un packagecurl -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Évaluation des résultats de tests
Format de réponse
L’API renvoie les résultats au format XML :
<?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>Analyse des résultats en Bash
#!/bin/bashRESULTS_FILE="aunit_results.xml"
# Nombre total de testsTOTAL_TESTS=$(grep -c '<testMethod' "$RESULTS_FILE" || echo "0")
# Tests réussis (pas d'alertes)PASSED_TESTS=$(grep -c '<alerts/>' "$RESULTS_FILE" || echo "0")
# Tests échouésFAILED_TESTS=$((TOTAL_TESTS - PASSED_TESTS))
# Extraire la couvertureSTATEMENT_COVERAGE=$(grep -oP 'statementCoverage percentage="\K[^"]+' "$RESULTS_FILE" || echo "0")BRANCH_COVERAGE=$(grep -oP 'branchCoverage percentage="\K[^"]+' "$RESULTS_FILE" || echo "0")
echo "================================"echo "Rapport de tests ABAP Unit"echo "================================"echo "Total Tests: $TOTAL_TESTS"echo "Réussis: $PASSED_TESTS"echo "Échoués: $FAILED_TESTS"echo ""echo "Couverture d'instructions: ${STATEMENT_COVERAGE}%"echo "Couverture de branches: ${BRANCH_COVERAGE}%"echo "================================"
# Code de sortie pour CI/CDif [ "$FAILED_TESTS" -gt 0 ]; then echo "::error::$FAILED_TESTS test(s) échoué(s) !" exit 1fi
# Vérifier le seuil de couvertureTHRESHOLD=70if (( $(echo "$STATEMENT_COVERAGE < $THRESHOLD" | bc -l) )); then echo "::warning::Couverture ${STATEMENT_COVERAGE}% inférieure au seuil ${THRESHOLD}%"fi
exit 0Analyse des résultats avec 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': {} }
# Compter les tests 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 # Collecter les détails d'erreur 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) })
# Extraire la couverture 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))
# Sortir avec code d'erreur si tests échoués sys.exit(1 if results['failed'] > 0 else 0)Intégration CI/CD avec GitHub Actions
Workflow GitHub Actions complet
name: Tests ABAP Unit
on: push: branches: [ main, develop ] pull_request: branches: [ main ]
env: ABAP_PACKAGE: Z_FLIGHT_BOOKING COVERAGE_THRESHOLD: 70
jobs: unit-tests: name: Exécuter tests ABAP Unit 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: Créer configuration de test 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: Exécuter tests ABAP Unit 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::Échec appel API avec HTTP $HTTP_CODE" cat aunit_results.xml exit 1 fi
echo "Appel API réussi"
- name: Analyser résultats de tests 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
# Couverture 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}, Réussis: {passed}, Échoués: {failed}") print(f"Couverture d'instructions: {stmt_cov}%")
if failed > 0: print("::error::Certains tests ont échoué !") exit(1) PYTHON_SCRIPT
- name: Vérifier seuil de couverture run: | COVERAGE=${{ steps.parse.outputs.coverage }} THRESHOLD=${{ env.COVERAGE_THRESHOLD }}
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then echo "::warning::Couverture ${COVERAGE}% inférieure au seuil ${THRESHOLD}%" fi
- name: Télécharger résultats de tests uses: actions/upload-artifact@v4 with: name: aunit-results path: aunit_results.xml
- name: Créer résumé de tests if: always() run: | cat >> $GITHUB_STEP_SUMMARY << EOF ## Résultats des tests ABAP Unit
| Métrique | Valeur | |--------|-------| | Total Tests | ${{ steps.parse.outputs.total }} | | Réussis | ${{ steps.parse.outputs.passed }} | | Échoués | ${{ steps.parse.outputs.failed }} | | Couverture | ${{ steps.parse.outputs.coverage }}% |
EOFWorkflow réutilisable pour plusieurs packages
name: Tests ABAP Unit (Réutilisable)
on: workflow_call: inputs: package: required: true type: string description: 'Package ABAP à tester" coverage_threshold: required: false type: number default: 70 description: 'Pourcentage minimum de couverture" include_dangerous: required: false type: boolean default: true description: 'Inclure les tests RISK LEVEL DANGEROUS" secrets: ABAP_ENDPOINT: required: true ABAP_USER: required: true ABAP_PASSWORD: required: true outputs: total: description: 'Nombre total de tests" value: ${{ jobs.test.outputs.total }} passed: description: 'Nombre de tests réussis" value: ${{ jobs.test.outputs.passed }} coverage: description: 'Pourcentage de couverture d'instructions" 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: Exécuter tests ABAP Unit pour ${{ 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: Analyser résultats id: parse run: | # Logique d'analyse ici (comme ci-dessus) echo "total=10" >> $GITHUB_OUTPUT echo "passed=9" >> $GITHUB_OUTPUT echo "coverage=85.5" >> $GITHUB_OUTPUTUtilisation du workflow réutilisable
name: Pipeline CI
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: Résumé des tests run: | echo "Tests Booking: ${{ needs.test-booking.outputs.passed }}/${{ needs.test-booking.outputs.total }}" echo "Tests Customer: ${{ needs.test-customer.outputs.passed }}/${{ needs.test-customer.outputs.total }}"Exécution de tests spécifiques
Tests pour classes individuelles
# Uniquement tests pour une classe spécifiquecurl -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 avec niveau de risque spécifique
<!-- Uniquement tests HARMLESS (pour feedback rapide) --><testRiskCoverage> <harmless active="true"/> <dangerous active="false"/> <critical active="false"/></testRiskCoverage>
<!-- Tous les tests y compris DANGEROUS (build nocturne) --><testRiskCoverage> <harmless active="true"/> <dangerous active="true"/> <critical active="true"/></testRiskCoverage>Tests avec durée spécifique
<!-- Uniquement tests SHORT (pour Pull Requests) --><durationCoverage short="true" medium="false" long="false"/>
<!-- Tous les tests (pour build nocturne) --><durationCoverage short="true" medium="true" long="true"/>Analyse de la couverture de code
Récupérer les détails de couverture
Les informations de couverture sont incluses dans la réponse :
<coverage> <statementCoverage percentage="78.5"/> <branchCoverage percentage="65.2"/> <procedureCoverage percentage="92.0"/></coverage>Générer un rapport de couverture
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("Aucune donnée de couverture trouvée") 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("RAPPORT DE COUVERTURE DE CODE") print("=" * 50)
for metric, value in metrics.items(): status = "✅" if value >= threshold else "❌" print(f"{status} {metric}: {value:.1f}%")
print("=" * 50) print(f"Seuil: {threshold}%")
# Générer un badge pour README 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"\nURL du badge: {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))Dépannage
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Identifiants incorrects | Vérifier user/mot de passe |
| 403 Forbidden | Autorisations manquantes | Vérifier Communication Arrangement |
| 404 Not Found | Package inexistant | Vérifier nom du package |
| 500 Internal Error | Erreur système | Vérifier les logs système |
| Timeout | Tests trop longs | Utiliser le filtre de durée |
Conseils de débogage
# Sortie verbose pour débogagecurl -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.logAugmenter le timeout
# Définir le timeout à 5 minutescurl --max-time 300 \ -X POST \ "$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \ ...Bonnes pratiques
| Aspect | Recommandation |
|---|---|
| Structure des tests | Catégoriser les tests par Risk Level et Duration |
| CI/CD | SHORT/HARMLESS pour PRs, tous pour build nocturne |
| Couverture | Minimum 70%, objectif 80%+ |
| Gestion erreurs | Sortie d’erreur détaillée dans pipeline |
| Caching | Stocker les Service Keys en toute sécurité (Secrets) |
| Parallélisation | Tester plusieurs packages en parallèle |
| Artefacts | Sauvegarder les résultats comme artefacts |
| Notification | Notifier Slack/Teams en cas d’échec |
Ressources complémentaires
- Tests unitaires pour services RAP - Tests spécifiques à RAP
- CI/CD avec ABAP Cloud - Exemples de pipelines complets
- Tests unitaires ABAP dans ABAP Cloud - Fondamentaux des tests unitaires
- Automatisation ATC sur BTP - Intégrer ATC dans CI/CD