ABAP Unit - Test-Framework
In diesem Artikel geht es um das Testen von privaten Methoden und einfache Deaktivierung von abhängigen Komponenten im Code.
Inhaltsverzeichnis
SAP bietet mit dem Test-Framework weitere Werkzeuge und Möglichkeiten saubere Tests aufzubauen. Dazu zählen Test Doubles, SEAMs und das Testen von privaten Methoden, die letzten beiden Techniken schauen wir uns in diesem Artikel etwas näher an.
Testobjekt
Um das zu testende Objekt besser zu verstehen, haben wir dir hier den Quellcode noch einmal aufgeführt. Die Klasse besteht aus einer öffentlichen Methode und einer privaten Methode. Die private Methode beschafft Daten aus zwei Tabellen und gibt diese zurück. Die öffentliche Methode ermittelt die Daten und gibt diese über eine zusätzliche Komponente aus. Wenn die Ausgabe funktioniert hat, wird ein entsprechendes Rückgabe-Kennzeichen gesetzt.
CLASS zcl_bs_demo_private_access DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS:
display_tools
RETURNING VALUE(rd_displayed) TYPE abap_bool.
PROTECTED SECTION.
PRIVATE SECTION.
TYPES:
BEGIN OF ts_tools,
short_name TYPE zbs_dy_tools-short_name,
stock_quantity TYPE zbs_dy_tools-stock_quantity,
description TYPE zbs_dy_toolst-description,
END OF ts_tools,
tt_tools TYPE STANDARD TABLE OF ts_tools WITH EMPTY KEY.
METHODS:
get_tool_data
RETURNING VALUE(rt_tools) TYPE tt_tools.
ENDCLASS.
CLASS zcl_bs_demo_private_access IMPLEMENTATION.
METHOD display_tools.
DATA(lt_tools) = get_tool_data( ).
TRY.
rd_displayed = NEW zcl_test_display( )->display_generic_data( lt_tools ).
CATCH cx_sy_data_access_error.
rd_displayed = abap_false.
ENDTRY.
ENDMETHOD.
METHOD get_tool_data.
TRY.
DATA(ld_language) = cl_abap_context_info=>get_user_language_abap_format( ).
CATCH cx_abap_context_info_error.
RETURN.
ENDTRY.
SELECT tool~short_name, tool~stock_quantity, text~description
FROM zbs_dy_tools AS tool
LEFT OUTER JOIN zbs_dy_toolst AS text
ON text~short_name = tool~short_name
AND text~language = @ld_language
INTO TABLE @rt_tools.
ENDMETHOD.
ENDCLASS.
Private Methoden
In verschiedenen Situationen kann es wichtig sein, auch einmal die privaten Methoden und Attribute einer Klasse zu testen. Hier solltest du dir aber im Klaren sein, dass die stabilste Schnittstelle die öffentlichen Methoden (Public) der Klasse sind und bereits durch ein einfaches Refactoring der interne Teil der Klasse schnell einmal anders aussehen kann.
Wieso testet man also den privaten Teil von Klassen?
- “Schnittstellen-Klasse” - Besitzt meist eine Verarbeitungsmethode, die die gesamte Verarbeitung startet und hat sehr viele private Methoden mit entsprechender Logik
- Anwendung von Injection - Für die Backdoor Injection muss auf den privaten Teil der Klasse zugegriffen werden, mehr dazu in den folgenden Kapiteln
- Wichtige Methoden - Es gibt wichtige und kritische Methoden innerhalb der Klasse, die mitgetestet werden sollen
Nun betrachten wir das Beispiel oben und erachten die Datenermittlungs-Methode als besonders wichtig und wollen diese in einem separaten Testfall prüfen. Um auf die privaten Attribute und Methoden zugreifen zu können, müssen wir die Testklasse mit der globalen Klasse “befreunden”. Globale Freunde gibt es bereits sehr lange im OO Kontext in ABAP, lokale Freunde sind noch relativ neu und dienen vor allem für Testklassen.
" 1 - Friends on TOP
CLASS ltc_private_method DEFINITION DEFERRED.
CLASS zcl_bs_demo_private_access DEFINITION LOCAL FRIENDS ltc_private_method.
CLASS ltc_private_method DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_full_database FOR TESTING RAISING cx_static_check.
ENDCLASS.
" 2 - Friends after definition
CLASS zcl_bs_demo_private_access DEFINITION LOCAL FRIENDS ltc_private_method.
Du hast die Möglichkeiten die Freundschaft an oberster Stelle im Test-Include zu definieren, noch bevor du die Testklasse definierst (1) oder sie nach der Definition zu befreunden (2). Beide Möglichkeiten haben vor und Nachteile, so kann man die Definition weiter unten leicht übersehen oder wie im ersten Fall, muss man die Definition der Testklasse doppelt anlegen und vorher bekannt geben.
CLASS ltc_private_method IMPLEMENTATION.
METHOD get_full_database.
DATA(lo_cut) = NEW zcl_bs_demo_private_access( ).
DATA(lt_result) = lo_cut->get_tool_data( ).
cl_abap_unit_assert=>assert_not_initial( lt_result ).
ENDMETHOD.
ENDCLASS.
Bei der Implementierung des Testfalls können wir nun auf die privaten Methoden und Attribute zugreifen und so die Selektion einzeln testen, ohne die Ausgabe ausführen zu müssen.
SEAM
In manchen Fällen arbeiten wir vielleicht mit komplett altem Coding und müssten für eine saubere Testbarkeit viel Umstellen oder diese kann gar nicht hergestellt werden. In solchen Fällen müssen wir störende Aufrufe und Komponenten komplett deaktivieren. Dies geht am leichtesten mit dem Test-Seam. Hierbei handelt es sich um zwei kleine Statements, mit denen wir unser Ziel erreichen können. Für den Test passen wir die Methode “display_tools” an und setzen den Methodenaufruf in das Test-Seam “display”.
METHOD display_tools.
DATA(lt_tools) = get_tool_data( ).
TRY.
TEST-SEAM display.
rd_displayed = NEW zcl_bs_demo_display( )->display_generic_data( lt_tools ).
END-TEST-SEAM.
CATCH cx_sy_data_access_error.
rd_displayed = abap_false.
ENDTRY.
ENDMETHOD.
Das Coding ist nun entsprechend für unsere Testfälle vorbereitet. Jetzt geht es um die Implementierung der Testfälle und was eigentlich so ein Test-Seam bewirkt. Dazu haben wir für dich die gesamte Testklasse vorbereitet.
CLASS ltc_seam DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
no_seam FOR TESTING,
empty_seam FOR TESTING,
returning_seam FOR TESTING.
ENDCLASS.
CLASS ltc_seam IMPLEMENTATION.
METHOD empty_seam.
DATA(lo_cut) = NEW zcl_bs_demo_private_access( ).
TEST-INJECTION display.
END-TEST-INJECTION.
DATA(ld_result) = lo_cut->display_tools( ).
cl_abap_unit_assert=>assert_false( ld_result ).
ENDMETHOD.
METHOD no_seam.
DATA(lo_cut) = NEW zcl_bs_demo_private_access( ).
DATA(ld_result) = lo_cut->display_tools( ).
cl_abap_unit_assert=>assert_false( ld_result ).
ENDMETHOD.
METHOD returning_seam.
DATA(lo_cut) = NEW zcl_bs_demo_private_access( ).
TEST-INJECTION display.
rd_displayed = abap_true.
END-TEST-INJECTION.
DATA(ld_result) = lo_cut->display_tools( ).
cl_abap_unit_assert=>assert_true( ld_result ).
ENDMETHOD.
ENDCLASS.
Wie verhalten sich die einzelnen Methoden nun genau? An dieser Stelle wollen wir einmal das Verhalten aufschlüsseln:
- Methode no_seam - Es wird keine Test-Injection aufgerufen, damit wird der Code in der Methode ganz normal aufgerufen als würde das Test-Seam nicht vorhanden sein.
- Methode no_seam - Es wird keine Test-Injection aufgerufen, damit wird der Code in der Methode ganz normal aufgerufen als würde das Test-Seam nicht vorhanden sein.
- Methode emtpy_seam - Die Test-Injection in der Methode ist leer, damit wird das Test-Seam in der zu testenden Methode ebenfalls durch nichts ausgetauscht. Damit wird die abhängige Klasse nicht aufgerufen. Es sollte allerdings darauf geachtet werden, dass dann der Rückgabewert ebenfalls nicht gesetzt wird.
- Methode returning_seam - In der Test-Injection wird der Rückgabewert für die Methode gesetzt und die abhängige Klasse nicht mehr aufgerufen. Damit haben wir die abhängige Komponente komplett deaktiviert und der Test liefert uns immer ein ABAP_TRUE zurück.
Mit Test-Seams und Test-Injections ist es leicht möglich abhängige Komponenten in bestehendem Quellcode zu deaktivieren und durch “leichteren” Code auszutauschen. Für testbaren Code ist es nicht unbedingt das optimale Mittel der Wahl, da sich der Code entsprechend von Situation zu Situation unterschiedlich verhält. Es sollte sich daher von vornherein um testbaren Code handeln oder in Ausnahmefällen auf Bestands-Coding angewandt werden, um diesen testbar zu machen.
Fazit
Mit unseren Hinweisen stehen dir nun weitere Methoden und Mittel zur Verfügung, wie du deine Tests aufbauen kannst, um so effizienter und einfacher Code testen zu können.