ABAP Unit Runner sur BTP : Tests automatisés dans SAP BTP ABAP Environment

Catégorie
BTP
Publié
Auteur
Johannes

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 :

AspectManuelAutomatisé
ExécutionÀ la demande dans ADTÀ chaque commit
CohérenceVariableToujours identique
RapiditéLentRapide
CI/CDImpossibleIntégration complète
TraçabilitéLimitéeEntiè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 :

  1. SAP BTP ABAP Environment (instance Steampunk)
  2. Communication User avec les autorisations correspondantes
  3. Communication Arrangement pour les services ADT
  4. 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 :

  1. Ouvrir Fiori Launchpad → Communication Arrangements
  2. Nouveau → Choisir le scénario SAP_COM_0763
  3. Créer ou sélectionner un Communication System existant
  4. Configurer Communication User avec mot de passe
  5. 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/testruns

Format 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

OptionValeursDescription
coveragetrue/falseActiver la couverture de code
branchCoveragetrue/falseCouverture de branche en plus
harmlesstrue/falseTests avec Risk Level HARMLESS
dangeroustrue/falseTests avec Risk Level DANGEROUS
criticaltrue/falseTests avec Risk Level CRITICAL
shorttrue/falseTests avec Duration SHORT
mediumtrue/falseTests avec Duration MEDIUM
longtrue/falseTests avec Duration LONG

Exemple cURL

Terminal window
# Exécuter les tests ABAP Unit pour un package
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

É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

parse_aunit_results.sh
#!/bin/bash
RESULTS_FILE="aunit_results.xml"
# Nombre total de tests
TOTAL_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és
FAILED_TESTS=$((TOTAL_TESTS - PASSED_TESTS))
# Extraire la couverture
STATEMENT_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/CD
if [ "$FAILED_TESTS" -gt 0 ]; then
echo "::error::$FAILED_TESTS test(s) échoué(s) !"
exit 1
fi
# Vérifier le seuil de couverture
THRESHOLD=70
if (( $(echo "$STATEMENT_COVERAGE < $THRESHOLD" | bc -l) )); then
echo "::warning::Couverture ${STATEMENT_COVERAGE}% inférieure au seuil ${THRESHOLD}%"
fi
exit 0

Analyse des résultats avec 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': {}
}
# 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

.github/workflows/abap-unit-tests.yml
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 }}% |
EOF

Workflow réutilisable pour plusieurs packages

.github/workflows/abap-unit-reusable.yml
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_OUTPUT

Utilisation du workflow réutilisable

.github/workflows/main.yml
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

Terminal window
# Uniquement tests pour une classe spécifique
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 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

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("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

ErreurCauseSolution
401 UnauthorizedIdentifiants incorrectsVérifier user/mot de passe
403 ForbiddenAutorisations manquantesVérifier Communication Arrangement
404 Not FoundPackage inexistantVérifier nom du package
500 Internal ErrorErreur systèmeVérifier les logs système
TimeoutTests trop longsUtiliser le filtre de durée

Conseils de débogage

Terminal window
# Sortie verbose pour débogage
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

Augmenter le timeout

Terminal window
# Définir le timeout à 5 minutes
curl --max-time 300 \
-X POST \
"$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \
...

Bonnes pratiques

AspectRecommandation
Structure des testsCatégoriser les tests par Risk Level et Duration
CI/CDSHORT/HARMLESS pour PRs, tous pour build nocturne
CouvertureMinimum 70%, objectif 80%+
Gestion erreursSortie d’erreur détaillée dans pipeline
CachingStocker les Service Keys en toute sécurité (Secrets)
ParallélisationTester plusieurs packages en parallèle
ArtefactsSauvegarder les résultats comme artefacts
NotificationNotifier Slack/Teams en cas d’échec

Ressources complémentaires