ABAP Unit Testing: Testgetriebene Entwicklung

kategorie
ABAP-Statements
Veröffentlicht
autor
Johannes

ABAP Unit ist das integrierte Test-Framework für automatisierte Unit-Tests in ABAP. Es ermöglicht testgetriebene Entwicklung (TDD) und sichert die Codequalität durch wiederholbare, automatisierte Tests.

Grundkonzept

  • Testklassen sind lokale Klassen mit dem Zusatz FOR TESTING
  • Testmethoden sind Methoden mit dem Zusatz FOR TESTING
  • Assertions prüfen erwartete Ergebnisse mit CL_ABAP_UNIT_ASSERT
  • Tests werden über STRG+SHIFT+F10 oder das Kontextmenü ausgeführt

Syntax

CLASS ltc_test_class DEFINITION
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS: test_method FOR TESTING.
ENDCLASS.
CLASS ltc_test_class IMPLEMENTATION.
METHOD test_method.
" Test-Logik mit Assertions
cl_abap_unit_assert=>assert_equals(
act = actual_value
exp = expected_value
).
ENDMETHOD.
ENDCLASS.

Beispiele

1. Einfacher Unit Test

" Zu testende Klasse
CLASS lcl_calculator DEFINITION.
PUBLIC SECTION.
METHODS: add IMPORTING iv_a TYPE i iv_b TYPE i
RETURNING VALUE(rv_result) TYPE i.
ENDCLASS.
CLASS lcl_calculator IMPLEMENTATION.
METHOD add.
rv_result = iv_a + iv_b.
ENDMETHOD.
ENDCLASS.
" Testklasse
CLASS ltc_calculator DEFINITION
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_calculator. " CUT = Class Under Test
METHODS:
setup,
test_add_positive_numbers FOR TESTING,
test_add_negative_numbers FOR TESTING,
test_add_zero FOR TESTING.
ENDCLASS.
CLASS ltc_calculator IMPLEMENTATION.
METHOD setup.
" Wird vor jedem Test ausgeführt
mo_cut = NEW #( ).
ENDMETHOD.
METHOD test_add_positive_numbers.
DATA(lv_result) = mo_cut->add( iv_a = 5 iv_b = 3 ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 8
msg = 'Addition positiver Zahlen fehlgeschlagen'
).
ENDMETHOD.
METHOD test_add_negative_numbers.
DATA(lv_result) = mo_cut->add( iv_a = -5 iv_b = -3 ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = -8
).
ENDMETHOD.
METHOD test_add_zero.
DATA(lv_result) = mo_cut->add( iv_a = 10 iv_b = 0 ).
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 10
).
ENDMETHOD.
ENDCLASS.

2. Wichtige Assertion-Methoden

CLASS ltc_assertions DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
test_assert_equals FOR TESTING,
test_assert_true_false FOR TESTING,
test_assert_initial FOR TESTING,
test_assert_bound FOR TESTING,
test_assert_differs FOR TESTING,
test_assert_char_cp FOR TESTING,
test_fail FOR TESTING.
ENDCLASS.
CLASS ltc_assertions IMPLEMENTATION.
METHOD test_assert_equals.
" Werte vergleichen
cl_abap_unit_assert=>assert_equals(
act = 42
exp = 42
msg = 'Werte sind nicht gleich'
).
" Strukturen vergleichen
DATA: ls_act TYPE ty_person,
ls_exp TYPE ty_person.
ls_act = VALUE #( name = 'Max' age = 30 ).
ls_exp = VALUE #( name = 'Max' age = 30 ).
cl_abap_unit_assert=>assert_equals(
act = ls_act
exp = ls_exp
).
" Tabellen vergleichen
DATA: lt_act TYPE TABLE OF string,
lt_exp TYPE TABLE OF string.
lt_act = VALUE #( ( `A` ) ( `B` ) ).
lt_exp = VALUE #( ( `A` ) ( `B` ) ).
cl_abap_unit_assert=>assert_equals(
act = lt_act
exp = lt_exp
).
ENDMETHOD.
METHOD test_assert_true_false.
DATA: lv_flag TYPE abap_bool VALUE abap_true.
" Prüfen, ob wahr
cl_abap_unit_assert=>assert_true(
act = lv_flag
msg = 'Flag sollte true sein'
).
" Prüfen, ob falsch
cl_abap_unit_assert=>assert_false(
act = xsdbool( 1 = 2 )
msg = 'Ausdruck sollte false sein'
).
ENDMETHOD.
METHOD test_assert_initial.
DATA: lv_empty TYPE string,
lv_filled TYPE string VALUE 'Test'.
" Prüfen, ob initial (leer)
cl_abap_unit_assert=>assert_initial(
act = lv_empty
msg = 'Variable sollte initial sein'
).
" Prüfen, ob NICHT initial
cl_abap_unit_assert=>assert_not_initial(
act = lv_filled
msg = 'Variable sollte nicht initial sein'
).
ENDMETHOD.
METHOD test_assert_bound.
DATA: lo_object TYPE REF TO lcl_calculator,
lo_null TYPE REF TO lcl_calculator.
lo_object = NEW #( ).
" Prüfen, ob Referenz gebunden
cl_abap_unit_assert=>assert_bound(
act = lo_object
msg = 'Objektreferenz sollte gebunden sein'
).
" Prüfen, ob NICHT gebunden
cl_abap_unit_assert=>assert_not_bound(
act = lo_null
msg = 'Objektreferenz sollte NULL sein'
).
ENDMETHOD.
METHOD test_assert_differs.
" Prüfen, dass Werte unterschiedlich sind
cl_abap_unit_assert=>assert_differs(
act = 'ABC'
exp = 'XYZ'
msg = 'Werte sollten unterschiedlich sein'
).
ENDMETHOD.
METHOD test_assert_char_cp.
" Pattern-Matching (wie CP in IF)
cl_abap_unit_assert=>assert_char_cp(
act = 'Hello World'
exp = 'Hello*'
msg = 'String sollte mit Hello beginnen'
).
ENDMETHOD.
METHOD test_fail.
" Test absichtlich fehlschlagen lassen
IF 1 = 2.
cl_abap_unit_assert=>fail(
msg = 'Dieser Code sollte nie erreicht werden'
).
ENDIF.
ENDMETHOD.
ENDCLASS.

3. Setup und Teardown

CLASS ltc_lifecycle DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: gv_class_setup_done TYPE abap_bool.
DATA: mv_setup_done TYPE abap_bool.
CLASS-METHODS:
class_setup, " Einmal vor allen Tests
class_teardown. " Einmal nach allen Tests
METHODS:
setup, " Vor jedem Test
teardown, " Nach jedem Test
test_first FOR TESTING,
test_second FOR TESTING.
ENDCLASS.
CLASS ltc_lifecycle IMPLEMENTATION.
METHOD class_setup.
" Einmalige Initialisierung für alle Tests
" z.B. Testdaten in DB anlegen
gv_class_setup_done = abap_true.
ENDMETHOD.
METHOD class_teardown.
" Aufräumen nach allen Tests
" z.B. Testdaten aus DB löschen
ENDMETHOD.
METHOD setup.
" Vor JEDEM Testfall
" z.B. Objekt neu instanziieren
mv_setup_done = abap_true.
ENDMETHOD.
METHOD teardown.
" Nach JEDEM Testfall
" z.B. Variablen zurücksetzen
CLEAR mv_setup_done.
ENDMETHOD.
METHOD test_first.
cl_abap_unit_assert=>assert_true( gv_class_setup_done ).
cl_abap_unit_assert=>assert_true( mv_setup_done ).
ENDMETHOD.
METHOD test_second.
cl_abap_unit_assert=>assert_true( gv_class_setup_done ).
cl_abap_unit_assert=>assert_true( mv_setup_done ).
ENDMETHOD.
ENDCLASS.

4. Exception-Tests

CLASS lcl_validator DEFINITION.
PUBLIC SECTION.
METHODS: validate_age IMPORTING iv_age TYPE i
RAISING cx_parameter_invalid.
ENDCLASS.
CLASS lcl_validator IMPLEMENTATION.
METHOD validate_age.
IF iv_age < 0.
RAISE EXCEPTION TYPE cx_parameter_invalid
EXPORTING parameter = 'AGE'.
ENDIF.
ENDMETHOD.
ENDCLASS.
CLASS ltc_validator DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_validator.
METHODS:
setup,
test_valid_age FOR TESTING,
test_negative_age_raises FOR TESTING.
ENDCLASS.
CLASS ltc_validator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW #( ).
ENDMETHOD.
METHOD test_valid_age.
" Sollte keine Exception werfen
TRY.
mo_cut->validate_age( 25 ).
CATCH cx_parameter_invalid.
cl_abap_unit_assert=>fail( 'Unerwartete Exception' ).
ENDTRY.
ENDMETHOD.
METHOD test_negative_age_raises.
" Exception wird erwartet
TRY.
mo_cut->validate_age( -5 ).
cl_abap_unit_assert=>fail( 'Exception erwartet' ).
CATCH cx_parameter_invalid INTO DATA(lx_error).
" Erwartete Exception - Test erfolgreich
cl_abap_unit_assert=>assert_equals(
act = lx_error->parameter
exp = 'AGE'
).
ENDTRY.
ENDMETHOD.
ENDCLASS.

5. Test Doubles (Mocking)

" Interface für Dependency Injection
INTERFACE lif_database.
METHODS: get_customer IMPORTING iv_id TYPE i
RETURNING VALUE(rs_customer) TYPE ty_customer.
ENDINTERFACE.
" Produktive Implementierung
CLASS lcl_database DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_database.
ENDCLASS.
" Zu testende Klasse
CLASS lcl_customer_service DEFINITION.
PUBLIC SECTION.
METHODS: constructor IMPORTING io_db TYPE REF TO lif_database.
METHODS: get_customer_name IMPORTING iv_id TYPE i
RETURNING VALUE(rv_name) TYPE string.
PRIVATE SECTION.
DATA: mo_db TYPE REF TO lif_database.
ENDCLASS.
CLASS lcl_customer_service IMPLEMENTATION.
METHOD constructor.
mo_db = io_db.
ENDMETHOD.
METHOD get_customer_name.
DATA(ls_customer) = mo_db->get_customer( iv_id ).
rv_name = ls_customer-name.
ENDMETHOD.
ENDCLASS.
" Test Double (Mock)
CLASS ltd_database DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES: lif_database.
DATA: ms_mock_customer TYPE ty_customer.
ENDCLASS.
CLASS ltd_database IMPLEMENTATION.
METHOD lif_database~get_customer.
" Gibt Mock-Daten zurück statt DB-Zugriff
rs_customer = ms_mock_customer.
ENDMETHOD.
ENDCLASS.
" Testklasse mit Mock
CLASS ltc_customer_service DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_customer_service,
mo_mock_db TYPE REF TO ltd_database.
METHODS:
setup,
test_get_customer_name FOR TESTING.
ENDCLASS.
CLASS ltc_customer_service IMPLEMENTATION.
METHOD setup.
" Mock erstellen
mo_mock_db = NEW #( ).
mo_mock_db->ms_mock_customer = VALUE #(
id = 1
name = 'Test Kunde'
).
" CUT mit Mock injizieren
mo_cut = NEW #( io_db = mo_mock_db ).
ENDMETHOD.
METHOD test_get_customer_name.
DATA(lv_name) = mo_cut->get_customer_name( 1 ).
cl_abap_unit_assert=>assert_equals(
act = lv_name
exp = 'Test Kunde'
).
ENDMETHOD.
ENDCLASS.

6. Test-Attribute

CLASS ltc_attributes DEFINITION FOR TESTING
DURATION MEDIUM " SHORT | MEDIUM | LONG
RISK LEVEL DANGEROUS. " HARMLESS | DANGEROUS | CRITICAL
PRIVATE SECTION.
METHODS:
" Kategorisierung
test_quick FOR TESTING,
" Test überspringen
test_not_yet_implemented FOR TESTING.
ENDCLASS.
CLASS ltc_attributes IMPLEMENTATION.
METHOD test_quick.
" Normaler Test
cl_abap_unit_assert=>assert_true( abap_true ).
ENDMETHOD.
METHOD test_not_yet_implemented.
" Test als "noch nicht implementiert" markieren
cl_abap_unit_assert=>skip( 'Noch nicht implementiert' ).
ENDMETHOD.
ENDCLASS.

7. Testdaten mit Helper-Methoden

CLASS ltc_with_helpers DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
test_process_orders FOR TESTING,
" Helper-Methoden (nicht FOR TESTING)
given_orders RETURNING VALUE(rt_orders) TYPE ty_orders,
given_customer RETURNING VALUE(rs_customer) TYPE ty_customer.
ENDCLASS.
CLASS ltc_with_helpers IMPLEMENTATION.
METHOD test_process_orders.
" Given
DATA(lt_orders) = given_orders( ).
DATA(ls_customer) = given_customer( ).
" When
DATA(lv_result) = process( lt_orders ).
" Then
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = 'OK'
).
ENDMETHOD.
METHOD given_orders.
rt_orders = VALUE #(
( id = 1 amount = 100 )
( id = 2 amount = 200 )
).
ENDMETHOD.
METHOD given_customer.
rs_customer = VALUE #( id = 1 name = 'Test' ).
ENDMETHOD.
ENDCLASS.

8. Tests für private Methoden

" Zu testende Klasse
CLASS lcl_processor DEFINITION
FRIENDS ltc_processor. " Testklasse als Friend deklarieren
PUBLIC SECTION.
METHODS: process RETURNING VALUE(rv_result) TYPE string.
PRIVATE SECTION.
METHODS: calculate_internal RETURNING VALUE(rv_value) TYPE i.
ENDCLASS.
" Testklasse kann auf private Methoden zugreifen
CLASS ltc_processor DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA: mo_cut TYPE REF TO lcl_processor.
METHODS:
setup,
test_calculate_internal FOR TESTING.
ENDCLASS.
CLASS ltc_processor IMPLEMENTATION.
METHOD setup.
mo_cut = NEW #( ).
ENDMETHOD.
METHOD test_calculate_internal.
" Direkter Zugriff auf private Methode durch FRIENDS
DATA(lv_value) = mo_cut->calculate_internal( ).
cl_abap_unit_assert=>assert_equals(
act = lv_value
exp = 42
).
ENDMETHOD.
ENDCLASS.

9. SQL Test Double Framework

CLASS ltc_with_sql_double DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: mo_environment TYPE REF TO if_osql_test_environment.
CLASS-METHODS:
class_setup,
class_teardown.
METHODS:
setup,
test_read_customer FOR TESTING.
ENDCLASS.
CLASS ltc_with_sql_double IMPLEMENTATION.
METHOD class_setup.
" Test-Environment für DB-Tabellen erstellen
mo_environment = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( 'KNA1' ) )
).
ENDMETHOD.
METHOD class_teardown.
mo_environment->destroy( ).
ENDMETHOD.
METHOD setup.
" Testdaten einfügen
DATA: lt_kna1 TYPE TABLE OF kna1.
lt_kna1 = VALUE #(
( mandt = sy-mandt kunnr = '0000001000' name1 = 'Test Kunde' )
).
mo_environment->insert_test_data( lt_kna1 ).
ENDMETHOD.
METHOD test_read_customer.
" SELECT liest jetzt aus Test Double statt echter DB
SELECT SINGLE name1 FROM kna1
WHERE kunnr = '0000001000'
INTO @DATA(lv_name).
cl_abap_unit_assert=>assert_equals(
act = lv_name
exp = 'Test Kunde'
).
ENDMETHOD.
ENDCLASS.

Test-Ausführung

" In Eclipse/ADT:
STRG + SHIFT + F10 " Alle Tests ausführen
Rechtsklick → Run As → ABAP Unit Test
" In SE80:
Rechtsklick → Unit Test ausführen

DURATION und RISK LEVEL

DURATIONErwartete Laufzeit
SHORT< 1 Sekunde
MEDIUM< 5 Sekunden
LONG> 5 Sekunden
RISK LEVELBeschreibung
HARMLESSKeine Datenbankänderungen
DANGEROUSMögliche Testdaten-Änderungen
CRITICALProduktionsdaten könnten betroffen sein

Wichtige Hinweise / Best Practice

  • Testklassen in lokalen Klassen (Definition/Implementation am Ende der Klasse).
  • Naming: ltc_ für Testklassen, ltd_ für Test Doubles, test_ für Methoden.
  • AAA-Pattern: Arrange (Given), Act (When), Assert (Then).
  • Ein Assert pro Test - fokussierte Tests sind besser wartbar.
  • Setup/Teardown für wiederholte Initialisierung/Aufräumen nutzen.
  • Dependency Injection für testbare Architektur mit Mocks.
  • FRIENDS ermöglicht Tests privater Methoden (sparsam verwenden).
  • cl_abap_unit_assert=>skip() für noch nicht implementierte Tests.
  • SQL Test Double Framework für Datenbank-unabhängige Tests.
  • Tests regelmäßig ausführen - idealerweise in CI/CD-Pipeline.