ABAP Unit Runner en BTP: Tests automatizados en SAP BTP ABAP Environment

Kategorie
BTP
Veröffentlicht
Autor
Johannes

Los Unit Tests automatizados son un componente central del desarrollo de software moderno. En SAP BTP ABAP Environment puedes ejecutar ABAP Unit Tests a traves de APIs e integrarlos en pipelines CI/CD. Este articulo te muestra como configurar y usar el ABAP Unit Runner.

Por que tests automatizados en BTP?

En un entorno cloud, el testing continuo es indispensable:

AspectoManualAutomatizado
EjecucionBajo demanda en ADTEn cada commit
ConsistenciaVariableSiempre igual
VelocidadLentaRapida
CI/CDNo posibleCompletamente integrado
TrazabilidadLimitadaCompletamente documentada
+----------------------------------------------------------------+
| Pipeline CI/CD con ABAP Unit Runner |
+----------------------------------------------------------------+
| |
| Git Push --> Build --> ABAP Unit --> ATC --> Deploy |
| Runner API API |
| | |
| v |
| +----------+ |
| | BTP | |
| | ABAP | |
| | System | |
| +----------+ |
| |
+----------------------------------------------------------------+

Requisitos previos

Antes de poder usar el ABAP Unit Runner necesitas:

  1. SAP BTP ABAP Environment (instancia Steampunk)
  2. Communication User con permisos correspondientes
  3. Communication Arrangement para servicios ADT
  4. Service Key para acceso API

Configurar Communication Arrangement

El Communication Arrangement permite el acceso externo a las 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 |
| +-- Autenticacion: Basic / OAuth 2.0 |
| +-- Password/Client Secret: ******** |
| |
| Inbound Services: |
| [x] ADT Core Services |
| [x] ADT ABAP Unit |
| [x] ADT ATC |
| |
+----------------------------------------------------------------+

Paso a paso:

  1. Abrir Fiori Launchpad -> Communication Arrangements
  2. Nuevo -> Seleccionar Scenario SAP_COM_0763
  3. Crear Communication System o seleccionar existente
  4. Configurar Communication User con password
  5. Activar Inbound Services

ADT REST API para Unit Tests

La ADT REST API permite la ejecucion programatica de ABAP Unit Tests.

Endpoint API

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

Formato del Request

El request usa un formato XML para configurar la ejecucion de 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>

Opciones de configuracion

OpcionValoresDescripcion
coveragetrue/falseActivar Code Coverage
branchCoveragetrue/falseBranch Coverage adicional
harmlesstrue/falseTests con Risk Level HARMLESS
dangeroustrue/falseTests con Risk Level DANGEROUS
criticaltrue/falseTests con Risk Level CRITICAL
shorttrue/falseTests con Duration SHORT
mediumtrue/falseTests con Duration MEDIUM
longtrue/falseTests con Duration LONG

Ejemplo cURL

Terminal window
# Ejecutar ABAP Unit Tests para un paquete
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

Evaluar resultados de tests

Formato del Response

La API devuelve los resultados en formato 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>

Parsing de resultados en Bash

parse_aunit_results.sh
#!/bin/bash
RESULTS_FILE="aunit_results.xml"
# Total de tests
TOTAL_TESTS=$(grep -c '<testMethod' "$RESULTS_FILE" || echo "0")
# Tests exitosos (sin alerts)
PASSED_TESTS=$(grep -c '<alerts/>' "$RESULTS_FILE" || echo "0")
# Tests fallidos
FAILED_TESTS=$((TOTAL_TESTS - PASSED_TESTS))
# Extraer Coverage
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 para CI/CD
if [ "$FAILED_TESTS" -gt 0 ]; then
echo "::error::$FAILED_TESTS test(s) failed!"
exit 1
fi
# Verificar threshold de Coverage
THRESHOLD=70
if (( $(echo "$STATEMENT_COVERAGE < $THRESHOLD" | bc -l) )); then
echo "::warning::Coverage ${STATEMENT_COVERAGE}% is below threshold ${THRESHOLD}%"
fi
exit 0

Parsing de resultados con 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': {}
}
# Contar 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
# Recopilar detalles de error
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)
})
# Extraer Coverage
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))
# Salir con codigo de error si hay tests fallidos
sys.exit(1 if results['failed'] > 0 else 0)

Integracion CI/CD con GitHub Actions

Workflow completo de GitHub Actions

.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

Workflow reutilizable para multiples paquetes

.github/workflows/abap-unit-reusable.yml
name: ABAP Unit Tests (Reusable)
on:
workflow_call:
inputs:
package:
required: true
type: string
description: 'Paquete ABAP a testear'
coverage_threshold:
required: false
type: number
default: 70
description: 'Porcentaje minimo de coverage'
include_dangerous:
required: false
type: boolean
default: true
description: 'Incluir tests RISK LEVEL DANGEROUS'
secrets:
ABAP_ENDPOINT:
required: true
ABAP_USER:
required: true
ABAP_PASSWORD:
required: true
outputs:
total:
description: 'Numero total de tests'
value: ${{ jobs.test.outputs.total }}
passed:
description: 'Numero de tests exitosos'
value: ${{ jobs.test.outputs.passed }}
coverage:
description: 'Porcentaje de statement coverage'
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: |
# Logica de parsing aqui (como arriba)
echo "total=10" >> $GITHUB_OUTPUT
echo "passed=9" >> $GITHUB_OUTPUT
echo "coverage=85.5" >> $GITHUB_OUTPUT

Ejecutar tests especificos

Tests para clases individuales

Terminal window
# Solo tests para una clase especifica
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 con Risk Level especifico

<!-- Solo tests HARMLESS (para feedback rapido) -->
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="false"/>
<critical active="false"/>
</testRiskCoverage>
<!-- Todos los tests incluyendo DANGEROUS (Nightly Build) -->
<testRiskCoverage>
<harmless active="true"/>
<dangerous active="true"/>
<critical active="true"/>
</testRiskCoverage>

Tests con Duration especifica

<!-- Solo tests SHORT (para Pull Requests) -->
<durationCoverage short="true" medium="false" long="false"/>
<!-- Todos los tests (para Nightly Build) -->
<durationCoverage short="true" medium="true" long="true"/>

Analizar Code Coverage

Obtener detalles de Coverage

La informacion de Coverage esta incluida en el Response:

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

Generar reporte de Coverage

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("No se encontraron datos de Coverage")
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 = "[OK]" if value >= threshold else "[X]"
print(f"{status} {metric}: {value:.1f}%")
print("=" * 50)
print(f"Threshold: {threshold}%")
# Generar badge para 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"\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

Errores frecuentes

ErrorCausaSolucion
401 UnauthorizedCredenciales incorrectasVerificar user/password
403 ForbiddenPermisos faltantesVerificar Communication Arrangement
404 Not FoundPaquete no existeVerificar nombre del paquete
500 Internal ErrorError del sistemaVerificar logs del sistema
TimeoutTests muy largosUsar filtro de Duration

Tips de debugging

Terminal window
# Output verbose para 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

Aumentar timeout

Terminal window
# Establecer timeout a 5 minutos
curl --max-time 300 \
-X POST \
"$ABAP_ENDPOINT/sap/bc/adt/abapunit/testruns" \
...

Mejores practicas

AspectoRecomendacion
Estructura de testsCategorizar tests por Risk Level y Duration
CI/CDSHORT/HARMLESS para PRs, todos para Nightly
CoverageMinimo 70%, objetivo 80%+
Manejo de erroresSalida de errores detallada en pipeline
CachingAlmacenar Service Keys de forma segura (Secrets)
ParalelizacionTestear multiples paquetes en paralelo
ArtefactosGuardar resultados como artefactos
NotificacionNotificar a Slack/Teams en caso de errores

Recursos adicionales