ABAP Unit - Testbarer Code (Teil 3)
Hier schauen wir uns die Möglichkeiten etwas genauer an, wie man abhängige Komponenten im eigenen Coding zur Testlaufzeit deaktivieren können.
Inhaltsverzeichnis
In diesem Artikel geht es um die Methodik der Dependency Injection, um verwendete Komponenten während des Tests zu deaktivieren und so den eigenen Code testbar zu machen.
Begriff
Hierbei geht es um die Technik, wie man mit abhängigen Komponenten umgeht und dem Tester bzw. Aufrufer die Möglichkeit gibt, solche Komponenten zu deaktivieren oder zu überschreiben.
Dazu einmal ein “schlechtes” Beispiel, bei dem der Aufrufer kaum eine Chance hat, die abhängige Komponente zu deaktivieren. Das Objekt wird in der Klasse instanziiert und direkt im Anschluss verwendet:
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
ENDMETHOD.
METHOD call_selector.
NEW zcl_test_doc( )->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
An Hand dieses Beispiels wirst du nun verschiedene Methoden lernen, wie du die Komponente durch deine eigene Komponente austauschen kannst. Hierbei werden wir einen Test Double verwenden.
Vorbereitung
Damit die stark gekoppelte Klasse erst einmal in der Methode entkoppelt wird, muss die Klasse etwas umgebaut werden. Die Instanzvariable sollte als Membervariable (Attribut der Klasse) ausgelagert werden und die Instanziierung sollte entsprechend von der Verwendung getrennt werden. Nach dem Umbau würde die Klasse dann wie folgt aussehen:
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string.
PRIVATE SECTION.
DATA:
mo_doc TYPE REF TO zif_test_doc.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
mo_doc = NEW zcl_test_doc( ).
ENDMETHOD.
METHOD call_selector.
mo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
Ebenfalls legen wir im lokalen Bereich eine Dummy Implementierung an, die den DOC austauscht und entsprechend unserer Logik zur Verfügung stellt. Am Namen “LTD_” (Local Test Double) erkennst du bereits, was der Sinn der Klasse ist.
CLASS ltd_doc DEFINITION FINAL.
PUBLIC SECTION.
INTERFACES: zif_test_doc.
ENDCLASS.
CLASS ltd_doc IMPLEMENTATION.
METHOD zif_test_doc~do_stuff_and_select_data.
" Empty implementation
ENDMETHOD.
ENDCLASS.
Constructor Injection
Über den Konstruktor bieten wir einen optionalen Parameter an, der die DOC überschreibt. Wir kein eigenes Objekt übergeben, dann wird eine entsprechende Instanz der konkreten Klasse erzeugt.
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING
io_doc TYPE REF TO zif_test_doc OPTIONAL,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA:
mo_doc TYPE REF TO zif_test_doc.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
mo_doc = io_doc.
IF mo_doc IS NOT BOUND.
mo_doc = NEW zcl_test_doc( ).
ENDIF.
ENDMETHOD.
METHOD call_selector.
mo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
Die Testklasse mit der Anwendung der Methode sieht nun wie folgt aus:
CLASS ltc_constructor_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS ltc_constructor_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( lo_local_doc ).
DATA(ld_result) = lo_cut->call_selector( abap_false ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Wir können beim Erzeugen des CUT nun unser eigenes Test Double übergeben und kontrollieren damit das Verhalten unsere Codes beim Test.
Setter Injection
Die Klasse stellt eine eigene Setter Methode zur Verfügung mit deren Hilfe man die DOC ersetzen kann. Diese Methode kann dann vor der Verwendung des CUT aufgerufen werden.
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string,
set_component
IMPORTING
io_doc TYPE REF TO zif_test_doc.
PROTECTED SECTION.
PRIVATE SECTION.
DATA:
mo_doc TYPE REF TO zif_test_doc.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
mo_doc = NEW zcl_test_doc( ).
ENDMETHOD.
METHOD call_selector.
mo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
METHOD set_component.
mo_doc = io_doc.
ENDMETHOD.
ENDCLASS.
Die Testklasse mit der Anwendung der Methode sieht nun wie folgt aus:
CLASS ltc_setter_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS ltc_setter_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( ).
lo_cut->set_component( lo_local_doc ).
DATA(ld_result) = lo_cut->call_selector( abap_false ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Parameter Injection
Die Parameter Injection arbeitet mit einem optionalen Übergabeparameter beim Aufruf der Methode. Wird eine Instanz übergeben, dann wird diese verwendet, ansonsten einfach eine neue Instanz der verwendeten Klasse erzeugt.
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
io_doc TYPE REF TO zif_test_doc OPTIONAL
RETURNING VALUE(rd_result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
ENDMETHOD.
METHOD call_selector.
DATA(lo_doc) = CAST zif_test_doc( COND #(
WHEN io_doc IS BOUND
THEN io_doc
ELSE NEW zcl_test_doc( )
) ).
lo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
Die Testklasse mit der Anwendung der Methode sieht nun wie folgt aus:
CLASS ltc_parameter_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS ltc_parameter_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( ).
DATA(ld_result) = lo_cut->call_selector(
id_flag = abap_false
io_doc = lo_local_doc ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Backdoor Injection
Bei der Backdoor Injection muss erst einmal nichts in der globalen Klasse angepasst werden. Die Backdoor wird über den FRIEND Zusatz erzeugt. Die Testklasse mit der Anwendung der Methode sieht nun wie folgt aus:
CLASS ltc_backdoor_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS zcl_test_handle_docs DEFINITION LOCAL FRIENDS ltc_backdoor_injection.
CLASS ltc_backdoor_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( ).
lo_cut->mo_doc = lo_local_doc.
DATA(ld_result) = lo_cut->call_selector( abap_false ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Verwendung
Mit den vier Injection Mechanismen stehen dir einige Mittel zur Verfügung, die du während der Entwicklung einsetzen kannst. In den häufigsten Fällen werden die Constructor und die Backdoor Injection eingesetzt, da sie den geringsten Aufwand versprechen und flexibel für den Test sind. Wichtig dabei ist nur, die Erzeugung von der Verwendung der Klasse zu trennen.
Wenn du eigene Vorstellungen für den Injection Mechanismus hast, dann kannst du natürlich auch diesen verwenden. Diese bekannten Patterns sind aber am Geläufigsten.
Fazit
Es gibt verschiedene Möglichkeiten abhängige Komponenten zu deaktivieren, aber immer eine Möglichkeit deinen Code auch testbar zu gestalten. Denke bei der Entwicklung immer daran, dass ein anderer Entwickler vielleicht auch deine Komponenten testen oder verwenden möchte.