RAP - ABAP Unit (Business Objekt)
An dieser Stelle schauen wir uns einmal an, wie wir unser RAP Business Objekt automatisch testen können, um so alle wichtigen Funktionen per Knopfdruck zu prüfen.
Inhaltsverzeichnis
In einer älteren Serie haben wir uns bereits ABAP Unit angeschaut und wie du damit deinen Code automatisch testen lassen kannst. In diesem Artikel schauen wir uns an, wie du dein RAP Objekt validieren lassen kannst und was du am sinnvollsten prüfen kannst.
ABAP Unit
Unter dem Begriff fallen alle Methoden zum automatischen Testen von ABAP Code im System, dabei gibt es verschiedene Techniken den Code testbar zu machen. Auf unserer Übersichtsseite findest du alle zu ABAP Unit und wie du es anwenden kannst, in diesem Artikel setzen wir das Wissen als Grundlage voraus.
Test schreiben
Für die Testfälle legen wir nun eine globale Testklasse an. Wir könnten zwar eine lokale Testklasse anlegen, wir wollen aber nur die öffentlichen Schnittstellen unseres RAP Objekts testen und den Test vom Originalobjekt trennen. Dazu erstellen wir die folgende Testklasse als Hülle für die Tests:
"! @testing ZBS_I_RAPPartner
CLASS zcl_bs_demo_unit_rap DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS:
create_new_entry FOR TESTING,
fill_empty_streets FOR TESTING,
clear_empty_streets FOR TESTING.
ENDCLASS.
CLASS zcl_bs_demo_unit_rap IMPLEMENTATION.
METHOD create_new_entry.
ENDMETHOD.
METHOD fill_empty_streets.
ENDMETHOD.
METHOD clear_empty_streets.
ENDMETHOD.
ENDCLASS.
Die Testklasse legen wir als globale Klasse an, dazu müssen auch die entsprechenden Zusätze (FOR TESTING, DURATION, RISK LEVEL) angegeben werden. Über den ABAP Doc Kommentar verlinken wir die Testklasse mit dem Core Data Service. Dazu wollen wir drei Tests implementieren:
- Anlage eines neuen Eintrags und Prüfung des erzeugten Schlüssels
- Befüllung eines Eintrags mit leerer Straße
- Löschen aller Einträge mit leerer Straße
Neuer Eintrag
Der Test befüllt im ersten Schritt eine Tabelle mit neuen Daten und markiert die Fehler als relevant, dann fügen wir die neuen Datensätze ein und persistieren die Daten per Commit auf der Datenbank. Dann prüfen wir die Rückgabe, ob Fehler enthalten sind. Zuletzt lesen wir von der Datenbank, ob der neue Eintrag auch wirklich angelegt wurde.
DATA:
lt_new_partner TYPE TABLE FOR CREATE ZBS_I_RAPPartner.
lt_new_partner = VALUE #(
( partnername = 'Do it Yourself'
street = 'Waterloo Street 13'
city = 'London'
country = 'GB'
paymentcurrency = 'GBP'
%control-PartnerName = if_abap_behv=>mk-on
%control-Street = if_abap_behv=>mk-on
%control-City = if_abap_behv=>mk-on
%control-Country = if_abap_behv=>mk-on
%control-PaymentCurrency = if_abap_behv=>mk-on
)
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner CREATE FROM lt_new_partner
MAPPED DATA(ls_mapped).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
cl_abap_unit_assert=>assert_initial( ls_commit_reported-partner ).
cl_abap_unit_assert=>assert_initial( ls_commit_failed-partner ).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, name
WHERE name = 'Do it Yourself'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
Leere Straße befüllen
Im ersten Schritt übergeben wir einen Partner, für den wir die Aktion ausführen möchten, mit einem Commit bestätigen wir die Aktion und lesen dann von der Datenbank den aktuellen Status nach. Die Straße sollte nun auf "EMPTY" stehen und der Eintrag von der Datenbank gefunden werden.
DATA:
lt_fill_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~fillEmptyStreets.
lt_fill_streets = VALUE #(
( PartnerNumber = '2000000001' )
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE fillEmptyStreets FROM lt_fill_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, Street
WHERE partner = '2000000001'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
cl_abap_unit_assert=>assert_equals( act = ls_partner_found-street exp = 'EMPTY' ).
Löschen der leeren Einträge
Für die Ausführung der statischen Aktion müssen wir die Tabelle mit einem leeren Eintrag befüllen, damit die Logik überhaupt ausgeführt wird. Dann wird die Aktion ausgeführt und per Commit bestätigt. Zum Abschluss lesen wir noch einmal die Datenbank, ob es noch Datensätze mit EMPTY gibt.
DATA:
lt_clear_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~clearAllEmptyStreets.
INSERT INITIAL LINE INTO TABLE lt_clear_streets.
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE clearAllEmptyStreets FROM lt_clear_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT FROM zbs_dmo_partner
FIELDS partner
WHERE street = 'EMPTY'
INTO TABLE @DATA(lt_empty_streets).
cl_abap_unit_assert=>assert_subrc( exp = 4 ).
Nach dem Ausführen des Unit Tests erhalten wir nun ein Ergebnis, doch wird dieser Test immer konsistent sein und können wir immer die gleichen Daten für den Test erwarten? Im nächsten Abschnitt werden wir dieses Problem angehen.
Daten mocken
Die ersten Testfälle sind nun erstellt, laufen aber gegen die echten Daten in der Datenbank. Wenn sich diese Daten ändern oder Datensätze gelöscht werden, dann funktionieren unsere Testfälle nicht mehr. Diese Abhängigkeiten wollen wir nun entfernen, in dem wir die Daten auf der Datenbank zum Zeitpunkt des Tests durch neue Testdaten tauschen und somit immer die passenden Testdaten für unsere Testfälle zur Verfügung haben. Dazu arbeiten wir mit einem Test-Double für den dahinterliegenden Core Data Service, das heißt wir brauchen einen CDS Double.
Im ersten Schritt erweitern wir die PRIVATE SECTION und legen eine Variable an, um den Double zu halten. Weiterhin implementieren wir die Methoden CLASS_SETUP und CLASS_TEARDOWN, um die Daten für den Test vorzubereiten:
PRIVATE SECTION.
CLASS-DATA:
go_environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS:
class_setup RAISING cx_static_check,
class_teardown.
Dann erzeugen wir den Double für den Interface View auf Referenz der Tabelle und aktivieren die Umleitung auf den Test-Double im Framework. Die Teardown Methode wird nur noch implementiert, um am Ende den Test-Double wieder abzubauen.
METHOD class_setup.
DATA:
lt_partner TYPE STANDARD TABLE OF zbs_dmo_partner WITH EMPTY KEY.
go_environment = cl_cds_test_environment=>create(
i_for_entity = 'ZBS_I_RAPPARTNER'
i_dependency_list = VALUE #( ( name = 'ZBS_DMO_PARTNER' type ='TABLE' ) )
).
lt_partner = VALUE #(
( partner = '2000000001' name = 'Las Vegas Corp' country = 'US' payment_currency = 'USD' )
( partner = '2000000002' name = 'Gorillas' street = 'Main street 10' country = 'DE' payment_currency = 'EUR' )
( partner = '2000000003' name = 'Tomato Inc' street = 'EMPTY' country = 'AU' payment_currency = 'AUD' )
).
go_environment->insert_test_data( lt_partner ).
go_environment->enable_double_redirection( ).
ENDMETHOD.
METHOD class_teardown.
go_environment->destroy( ).
ENDMETHOD.
Für unseren Test implementieren wir einen anderen "Nummernkreis", um die Daten von den Originaldaten abzugrenzen. Weiterhin fügen wir nur einige Testdaten, um den Test übersichtlich zu halten.
Komplettes Beispiel
Hier noch einmal die gesamte Testklasse mit allen Testmethoden, wie immer findest du diese auch im Git-Repository der Serie:
"! @testing ZBS_I_RAPPartner
CLASS zcl_bs_demo_unit_rap DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
CLASS-DATA:
go_environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS:
class_setup RAISING cx_static_check,
class_teardown.
METHODS:
create_new_entry FOR TESTING,
fill_empty_streets FOR TESTING,
clear_empty_streets FOR TESTING.
ENDCLASS.
CLASS zcl_bs_demo_unit_rap IMPLEMENTATION.
METHOD class_setup.
DATA:
lt_partner TYPE STANDARD TABLE OF zbs_dmo_partner WITH EMPTY KEY.
go_environment = cl_cds_test_environment=>create(
i_for_entity = 'ZBS_I_RAPPARTNER'
i_dependency_list = VALUE #( ( name = 'ZBS_DMO_PARTNER' type ='TABLE' ) )
).
lt_partner = VALUE #(
( partner = '2000000001' name = 'Las Vegas Corp' country = 'US' payment_currency = 'USD' )
( partner = '2000000002' name = 'Gorillas' street = 'Main street 10' country = 'DE' payment_currency = 'EUR' )
( partner = '2000000003' name = 'Tomato Inc' street = 'EMPTY' country = 'AU' payment_currency = 'AUD' )
).
go_environment->insert_test_data( lt_partner ).
go_environment->enable_double_redirection( ).
ENDMETHOD.
METHOD class_teardown.
go_environment->destroy( ).
ENDMETHOD.
METHOD create_new_entry.
DATA:
lt_new_partner TYPE TABLE FOR CREATE ZBS_I_RAPPartner.
lt_new_partner = VALUE #(
( partnername = 'Do it Yourself'
street = 'Waterloo Street 13'
city = 'London'
country = 'GB'
paymentcurrency = 'GBP'
%control-PartnerName = if_abap_behv=>mk-on
%control-Street = if_abap_behv=>mk-on
%control-City = if_abap_behv=>mk-on
%control-Country = if_abap_behv=>mk-on
%control-PaymentCurrency = if_abap_behv=>mk-on
)
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner CREATE FROM lt_new_partner
MAPPED DATA(ls_mapped).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
cl_abap_unit_assert=>assert_initial( ls_commit_reported-partner ).
cl_abap_unit_assert=>assert_initial( ls_commit_failed-partner ).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, name
WHERE name = 'Do it Yourself'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
ENDMETHOD.
METHOD fill_empty_streets.
DATA:
lt_fill_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~fillEmptyStreets.
lt_fill_streets = VALUE #(
( PartnerNumber = '2000000001' )
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE fillEmptyStreets FROM lt_fill_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, Street
WHERE partner = '2000000001'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
cl_abap_unit_assert=>assert_equals( act = ls_partner_found-street exp = 'EMPTY' ).
ENDMETHOD.
METHOD clear_empty_streets.
DATA:
lt_clear_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~clearAllEmptyStreets.
INSERT INITIAL LINE INTO TABLE lt_clear_streets.
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE clearAllEmptyStreets FROM lt_clear_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT FROM zbs_dmo_partner
FIELDS partner
WHERE street = 'EMPTY'
INTO TABLE @DATA(lt_empty_streets).
cl_abap_unit_assert=>assert_subrc( exp = 4 ).
ENDMETHOD.
ENDCLASS.
Fazit
Die Implementierung eines Unit Test für ein RAP Business Objekt ist nicht schwer, aber erst mit dem Mocken der Daten von der Datenbank, können auch stabile Tests gebaut werden. Diese Tests sorgen am Ende auch für die sinnvolle Nutzung bei Erweiterungen des Objekts.