ABAP Unit - TDF (Function Double)
Schauen wir uns in diesem Artikel einmal an, wie wir mit Funktionsbausteinen als Abhängigkeit umgehen können, ohne diese mitzutesten.
Inhaltsverzeichnis
Der letzte Artikel zum Thema Unit Tests ist bereits eine ganze Weile her. Damals haben wir die Serie zum Thema automatisches Testen eigentlich schon abgeschlossen, doch in der Zwischenzeit hat SAP weitere Features zur Verfügung gestellt. Dabei gibt es nun einen Test-Double für Funktionsbausteine, den wir uns in diesem Artikel einmal näher anschauen wollen.
Einleitung
Ein Test-Double ist dafür da, Abhängigkeiten zu aufgerufenen Objekten zu eliminieren. Das heißt, immer wenn eine Datenbank angefragt, ein Service aufgerufen, eine andere Klasse gestartet oder ein Funktionsbaustein implementiert wird, erhält man Abhängigkeiten. Da man in einem Unit Test nur seinen eigenen Code prüfen möchte, sollten solche Abhängigkeiten zur Testlaufzeit verschwinden. Dafür gibt es das Konzept des Doubles, es wird die Abhängigkeit ausgetauscht und man erhält die Kontrolle über die Rückgabewerte. Tests sind wieder stabil und wiederholbar.
Vorbereitung
Dazu benötigen wir eine Abhängigkeit, im speziellen Fall einen Funktionsbaustein. Dazu legen wir uns einen Testbaustein an, der Rechenoperationen ausführt. Dabei implementieren wir nur die Operationen für die Addition und Subtraktion:
FUNCTION z_bs_demo_test_function
IMPORTING
VALUE(id_input_one) TYPE i
VALUE(id_input_two) TYPE i
VALUE(id_method) TYPE string
EXPORTING
VALUE(ed_result) TYPE i.
CASE id_method.
WHEN '+'.
ed_result = id_input_one + id_input_two.
WHEN '-'.
ed_result = id_input_one - id_input_two.
ENDCASE.
ENDFUNCTION.
Konfiguration
Zuerst einmal benötigen wir eine Variable auf globaler Ebene, die unser Test-Environment hält. Wie für Datenbanken und CDS Views gibt es ein eigenes Environment:
CLASS-DATA:
go_function TYPE REF TO if_function_test_environment.
Zuerst einmal initialisieren wir das Environment und übergeben die Funktionsbausteine, für die wir einen Test-Double anlegen wollen.
" Initialize double
go_function = cl_function_test_environment=>create( VALUE #( ( 'Z_BS_DEMO_TEST_FUNCTION' ) ) ).
Als Nächstes holen wir uns das Double das wir konfigurieren wollen, dies geschieht über den Namen des Funktionsbausteins.
" Get double for function module
DATA(lo_demo_function) = go_function->get_double( 'Z_BS_DEMO_TEST_FUNCTION' ).
Dann erstellen wir die Eingabe-Konfiguration, dieses Set definiert einen Zustand. Wenn alle Parameter wie definiert beim Aufruf des Funktionsbausteins übergeben werden, dann wird dieses Set ausgelöst.
" Configure input configuration
DATA(lo_input_config) = lo_demo_function->create_input_configuration(
)->set_importing_parameter( name = 'ID_INPUT_ONE' value = 10
)->set_importing_parameter( name = 'ID_INPUT_TWO' value = 10
)->set_importing_parameter( name = 'ID_METHOD' value = '*'
).
Passend zur Eingabe-Konfiguration, muss nun auch eine Ausgabe-Konfiguration definiert werden. Damit werden die Ausgabeparameter des Funktionsbausteins mit Werten versorgt.
" Configure output configuration
DATA(lo_output_config) = lo_demo_function->create_output_configuration(
)->set_exporting_parameter( name = 'ED_RESULT' value = 100
).
Im letzten Schritt müssen die beiden Konfigurationen nun zusammengebracht werden. Dafür konfigurieren wir den Aufruf, wenn die Eingabewerte der Vorgabe entsprechen, dann soll die Ausgabe entsprechend der Vorgabe gesetzt werden.
" Set matching configuration
lo_demo_function->configure_call( )->when( lo_input_config )->then_set_output( lo_output_config ).
Möglichkeiten
Neben der Möglichkeit direkt die Ausgabe zu setzen, kannst du auch anderes Verhalten simulieren, wie die Ausgabe von klassischen Ausnahmen, OO-Ausnahmen oder einer direkten Antwort. Damit verhält sich das Double auch wie der Test-Double für Klassen. Mehr Informationen erhältst du in der Schnittstelle:
Testfall
Definieren wir nun einen ersten abstrakten Testfall, um dieses Verhalten zu testen. Dazu rufen wir in unserem Testfall den Funktionsbaustein direkt auf. Normalerweise würden wir die globale Klasse testen und möchten eigentlich den Funktionsbaustein deaktivieren. Zur Veranschaulichung testen wir nur einmal den Funktionsbaustein, um das eigentliche Double zu testen:
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 10
id_input_two = 10
id_method = '*'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 100 ).
Die weiteren Testfälle legen wir an (siehe vollständiges Beispiel), wobei ein Testfall nicht funktionieren soll. Hier legen wir für eine Kombination keine Parameter an. Damit sollte keine Konfiguration gefunden werden. Damit funktionieren auch Szenarien, die eigentlich gar nicht im Funktionsbaustein definiert wurden:
Vollständiges Beispiel
Wie immer noch einmal das gesamte Beispiel der globalen Testklasse mit den entsprechenden Kommentaren und Definitionen:
"! @testing Z_BS_DEMO_TEST_FUNCTION
CLASS zcl_bs_demo_funcdouble DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
CLASS-DATA:
go_function TYPE REF TO if_function_test_environment.
CLASS-METHODS:
class_setup,
class_teardown.
METHODS:
not_defined_method FOR TESTING,
wrong_substraction FOR TESTING,
not_defined_case FOR TESTING.
ENDCLASS.
CLASS zcl_bs_demo_funcdouble IMPLEMENTATION.
METHOD class_setup.
go_function = cl_function_test_environment=>create( VALUE #( ( 'Z_BS_DEMO_TEST_FUNCTION' ) ) ).
DATA(lo_demo_function) = go_function->get_double( 'Z_BS_DEMO_TEST_FUNCTION' ).
" Configure test double - First call
DATA(lo_input_config) = lo_demo_function->create_input_configuration(
)->set_importing_parameter( name = 'ID_INPUT_ONE' value = 10
)->set_importing_parameter( name = 'ID_INPUT_TWO' value = 10
)->set_importing_parameter( name = 'ID_METHOD' value = '*'
).
DATA(lo_output_config) = lo_demo_function->create_output_configuration(
)->set_exporting_parameter( name = 'ED_RESULT' value = 100
).
lo_demo_function->configure_call( )->when( lo_input_config )->then_set_output( lo_output_config ).
" Configure test double - Second call
lo_input_config = lo_demo_function->create_input_configuration(
)->set_importing_parameter( name = 'ID_INPUT_ONE' value = 20
)->set_importing_parameter( name = 'ID_INPUT_TWO' value = 10
)->set_importing_parameter( name = 'ID_METHOD' value = '-'
).
lo_output_config = lo_demo_function->create_output_configuration(
)->set_exporting_parameter( name = 'ED_RESULT' value = 11
).
lo_demo_function->configure_call( )->when( lo_input_config )->then_set_output( lo_output_config ).
ENDMETHOD.
METHOD class_teardown.
go_function->clear_doubles( ).
ENDMETHOD.
METHOD not_defined_method.
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 10
id_input_two = 10
id_method = '*'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 100 ).
ENDMETHOD.
METHOD wrong_substraction.
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 20
id_input_two = 10
id_method = '-'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 11 ).
ENDMETHOD.
METHOD not_defined_case.
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 15
id_input_two = 20
id_method = '+'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 35 ).
ENDMETHOD.
ENDCLASS.
Fazit
Nun ist es auch auf leichtem Weg möglich, Funktionsbausteine in unseren Objekten mitzutesten und dabei auf Test-Seams zu verzichten. Funktionsbausteine werden in Zukunft zwar immer weniger eingesetzt werden, doch solange sie existieren, sollte es auch eine Möglichkeit geben, sie zu mocken.