Testautomatisierung von OData Services in Zeiten von Continuous Integration und Continuous Delivery ist essentiell. Die Gründe sind, dass eigenentwickelte SAPUI5 Apps zunehmend geschäftskritische Funktionalitäten implementieren und der manuelle Test zu aufwendig ist. Das ABAP OData Framework ist mit dem ABAP Unit Test Framework integriert. OData ABAP Unit Test wird in der Online Doku SAP Gateway Foundation nur sehr kurz beschrieben. Dennoch sieht es auf dem 1. Blick sehr einfach aus. In diesem Blog wollen wir OData ABAP Unit Test am Beispiel des GWSAMPLE_BASIC OData Service einmal ausprobieren. Die Funktionalität des GWSAMPLE_BASIC Service ist in der Data Provider Class /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT implementiert ( Siehe SEGW-Projekt /IWBEP/GWSAMPLE_BASIC ).
Integration einer Data Provider Class mit dem Unit Test Framework
Die Unit Test Integration zwischen der DPC-Klasse besteht SAP-seitig aus 2 Teilen:
- Methode /iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test: Jede DPC-Klasse erbt die Implementierung aus Klasse /IWBEP/CL_MGW_ABS_DATA. Sie dient dazu, ein DPC-Objekt so zu parametrisieren, dass es durch einen Unit Test aufrufbar ist.
- Klasse /IWBEP/CL_MGW_REQUEST_UNITTST: Die Klasse ist eine Fassade für den OData Request. Ein Objekt dieser Klasse wird aus dem init_dp_for_unit_test an den Unit Test zurückgeliefert und muss dann an die zu testende CRUDQ-Methode der DPC-Klasse übergeben werden.
Die Abbildung zeigt ausgehend von der DPC-Klasse des GWSAMPLE_BASIC einen Ausschnitt der Implementierung der Methode init_dp_for_unit_test.
Hands On
Um die Integration der DPC-Klasse mit dem ABAP Unit Test Framework auszuprobieren, lege eine Klasse ZCL_RL_ODATA_UNITTEST an. Der Klassentyp ist Testklasse und die Instanzerzeugung ist abstrakt. Im Regelfall wird keine globale Testklasse angelegt, weil der Unit Test Code als lokale Klasse bei der zu testenden Klasse hinterlegt ist. Die lokale Unit Test Klasse LCL_TEST enthält die Logik, um die Data Provider Class /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT zu testen.
Das Ergebnis sollte hiernach so aussehen. Die Unit Test Klasse besteht aus Methoden (Prefix TEST), welche die DPC-Funktionalität testen. Alle anderen sind Helper Methoden wie READ_ODATA_MODEL, welche durch die Testmethoden genutzt werden.
Für den Unit Test verwende ich die Entity BusinessPartner. Im Folgenden erkläre ich die Funktionalität der TEST-Methoden.
OData Unit Test Read (test_businesspartner_read)
Die Methode init_dp_for_unit_test erwartet eine Struktur, die aus einer Vielzahl von Felder besteht. Der OData Request wird also nicht als String wie im Gateway Client in der Form /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet(‘0100000001’) übergeben. Die Befüllung der Felder ist abhängig von der CRUDQ-Methode. Immer zu füllen die Entity-Namen für Source und Target.
Durch Aufruf der DPC-Methode init_dp_for_unit_test wird der DPC für den Unit Test auf Basis der Struktur initialisiert. Beim Lesen einer Entity muss der Schlüssel über die Methode set_converted_keys in den Request geschrieben werden.
Die Member Variable mr_data_provider enthält ein DPC-Objekt. Über den Aufruf der /iwbep/if_mgw_appl_srv_runtime~get_entity führen wir die Anwendungslogik in Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, BUSINESSPARTNERS_GET_ENTITY aus.
Das wirkt alles ein wenig kompliziert, nicht wahr? Um besser zu verstehen, was hier eigentlich passiert, setze einen Breakpoint in diese Test Methode und debug einmal die Methode init_dp_for_unit_test und /iwbep/if_mgw_appl_srv_runtime~get_entity durch.
METHOD test_businesspartner_read. DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst. DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit. DATA lr_exc_base TYPE REF TO /iwbep/cx_mgw_base_exception. DATA ls_ent_businesspartner_exp TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner. FIELD-SYMBOLS <ls_ent_businesspartner_act> TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner. ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context_unit ). TRY. * Schlüssel des Business Partners in den Request aufnehmen ls_ent_businesspartner_exp-bp_id = '0100000001'. GET REFERENCE OF ls_ent_businesspartner_exp INTO DATA(lr_ent_businesspartner_exp). lr_request_unittst->set_converted_keys( lr_ent_businesspartner_exp ). * Dies ruft die zu testende Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, BUSINESSPARTNERS_GET_ENTITY auf mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~get_entity( EXPORTING io_tech_request_context = lr_request_unittst IMPORTING er_entity = DATA(lr_ent_businesspartner_act) ). ASSIGN lr_ent_businesspartner_act->* TO <ls_ent_businesspartner_act>. * Stimmt die erwartete BusPartnerId mit der tatsächlichen überein? cl_abap_unit_assert=>assert_equals( act = <ls_ent_businesspartner_act>-bp_id exp = ls_ent_businesspartner_exp-bp_id ). CATCH /iwbep/cx_mgw_base_exception INTO lr_exc_base. cl_abap_unit_assert=>fail( msg = lr_exc_base->get_text( ) ). ENDTRY. ENDMETHOD.
OData Unit Test Update (test_businesspartner_update)
Es wird ein kleines Stück komplexer. Beim Update lesen wir zunächst den Business Partner von der Datenbank in die Entity-Struktur. Danach müssen wir die Entity Struktur in einen Entity Provider verpacken. Der Rest des Codes ist analog zum Read.
METHOD test_businesspartner_update. DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst. DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit. DATA ls_ent_businesspartner_exp TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner. FIELD-SYMBOLS <ls_ent_businesspartner_act> TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner. DATA lr_entry_provider TYPE REF TO /iwbep/if_mgw_entry_provider. * Business Partner von der Datenbank lesen und die TelefonNr verändern ls_ent_businesspartner_exp = read_business_partner( '0100000001' ). ls_ent_businesspartner_exp-phone_number = sy-uzeit. * Der Update erwartet die Daten verpackt in einen sogeannten Entity Provider lr_entry_provider = conv_entity_to_entry_provider( ls_ent_businesspartner_exp ). * Basisinformationen des OData Requests füllen ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. * Unit Test Request Objekt erzeugen lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context_unit ). * Schlüssel des zu ändernden Business Partners in den Request aufnehmen GET REFERENCE OF ls_ent_businesspartner_exp INTO DATA(lr_ent_businesspartner_exp). lr_request_unittst->set_converted_keys( lr_ent_businesspartner_exp ). TRY. * Business Partner ändern mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~update_entity( EXPORTING iv_entity_name = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }| iv_entity_set_name = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set| iv_source_name = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set| io_data_provider = lr_entry_provider io_tech_request_context = lr_request_unittst IMPORTING er_entity = DATA(lr_ent_business_partner_act) ). ASSIGN lr_ent_business_partner_act->* TO <ls_ent_businesspartner_act>. * Ist die Telefonnummer wirklich geändert? cl_abap_unit_assert=>assert_equals( act = <ls_ent_businesspartner_act>-phone_number exp = ls_ent_businesspartner_exp-phone_number ). CATCH /iwbep/cx_mgw_base_exception INTO DATA(lr_exc_base). cl_abap_unit_assert=>fail( msg = lr_exc_base->get_text( ) ). ENDTRY. ENDMETHOD.
Probleme bei der Intialisierung des DPC-Objekts (test_init_dp_fail)
Die von SAP bereitgestellte Methode init_dp_for_unit_test haben wir schon benutzt. Wir werden nun sehen, dass die Methode ganz schön zickig ist. Konkret heißt dies, dass ich in der Request Struktur keine Referenzen übergeben kann. Tut man dies dennoch, kommt es zu einer Ausnahme. Alle Referenzen in dieser Struktur wie z.B converted_keys können deshalb erst nach der Initialisierung in den OData Request gesetzt werden.
METHOD test_init_dp_fail. DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst. DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit. DATA ls_ent_businesspartner TYPE /iwbep/cl_gwsample_bas_mpc=>ts_businesspartner. * Das ist ok: Struktur enthält keine Referenzen lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( VALUE #( ) ). GET REFERENCE OF ls_ent_businesspartner INTO ls_request_context_unit-technical_request-converted_keys. * Das geht nicht: Jede Referenz (ref to data als auch ref to object) führt zu Serialisierungs-Ausnahme * Methode /IWBEP/CL_MGW_REQUEST_UNITTST, MOVE_CORRESPONDING * 'Im ST-Program /IWBEP/ST_ANY_DATA ist bei der Serialisierung ein Fehler aufgetreten.' * 'Die Verwendung eines Referenztyps ist hier nicht unterstützt.' lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context_unit ). ENDMETHOD.
OData Unit Test Query Teil 1(test_businesspartner_query)
Nun wollen wir einmal die Query /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet?$top=5&$skip=10 testen. Damit der Query funktioniert, braucht das DPC-Objekt Metainformationen aus dem OData-Model. Die Methode init_dp_for_unit_test sieht aber nicht vor, dass man ein Model übergeben kann. Ich habe deshalb den Code kopiert und in der lokalen Methode init_dp_for_unit_test erweitert.
METHOD test_businesspartner_query. DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit. DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst. FIELD-SYMBOLS TYPE /iwbep/cl_gwsample_bas_mpc=>tt_businesspartner. DATA lr_exc_base TYPE REF TO /iwbep/cx_mgw_base_exception. DATA lr_odata_model TYPE REF TO /iwbep/if_mgw_odata_fw_model. * Basisinformationen des OData Requests füllen ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. * Top und Skip im Request setzen ls_request_context_unit-paging-skip = 10. ls_request_context_unit-paging-top = 5. * OData Model lesen. Siehe Tabelle /IWBEP/I_MGW_OHD lr_odata_model = read_odata_model( iv_version = '0001' iv_technical_name = '/IWBEP/GWSAMPLE_BASIC_MDL' ). * DPC-Objekt init: Standard. Keine Übergabe des Models möglich * lr_request_unittst = mr_data_provider->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( is_request_context = ls_request_context_unit ). * DPC-Objekt init selbstgemacht lr_request_unittst = init_dp_for_unit_test( is_request_context = ls_request_context_unit ir_data_provider = mr_data_provider ir_odata_model = lr_odata_model ). TRY . * OData Query ausführen mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~get_entityset( EXPORTING io_tech_request_context = lr_request_unittst IMPORTING er_entityset = DATA(lr_entityset) ). ASSIGN lr_entityset->* TO . * Mit der Top und Skip Parametriesierung ohne weitere Einschränkungen kommen hier 5 Sätze zurück cl_abap_unit_assert=>assert_equals( act = lines( ) exp = 5 ). CATCH /iwbep/cx_mgw_base_exception INTO lr_exc_base. cl_abap_unit_assert=>fail( msg = lr_exc_base->get_text( ) ). ENDTRY. ENDMETHOD.
OData Unit Test Query Teil 2 (test_businesspartner_query_fai)
Jetzt lass uns einmal versuchen eine vollwertige Query wie /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet?$filter=BusinessPartnerID ge ‘0100000001’ and BusinessPartnerID le ‘0100000010’ zu implementieren.
Ich habe das nicht hingekriegt, diese Filterkriterien in den OData-Request reinzukriegen. Sie werden ignoriert mit dem Ergebnis, dass der Unit Test scheitert.
METHOD test_businesspartner_query_fai. DATA ls_request_context_unit TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit. DATA lt_select_option TYPE /iwbep/t_mgw_select_option. DATA lr_request_unittst TYPE REF TO /iwbep/cl_mgw_request_unittst. DATA lr_odata_model TYPE REF TO /iwbep/if_mgw_odata_fw_model. FIELD-SYMBOLS <lt_ent_businesspartner> TYPE /iwbep/cl_gwsample_bas_mpc=>tt_businesspartner. DATA lr_exc_base TYPE REF TO /iwbep/cx_mgw_base_exception. * Basisinformationen des OData Requests füllen ls_request_context_unit-technical_request-source_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-target_entity_type = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }|. ls_request_context_unit-technical_request-source_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. ls_request_context_unit-technical_request-target_entity_set = |{ /iwbep/cl_gwsample_bas_mpc=>gc_businesspartner }Set|. * OData Model lesen. Siehe Tabelle /IWBEP/I_MGW_OHD lr_odata_model = read_odata_model( iv_version = '0001' iv_technical_name = '/IWBEP/GWSAMPLE_BASIC_MDL' ). * Selektionsoptionen für BusinessPartnerID ge '0100000001' and BusinessPartnerID le '0100000010' aufbauen APPEND INITIAL LINE TO lt_select_option ASSIGNING FIELD-SYMBOL(<ls_select_option>). <ls_select_option>-property = 'BusinessPartnerID'. APPEND INITIAL LINE TO <ls_select_option>-select_options ASSIGNING FIELD-SYMBOL(<ls_rng>). <ls_rng>-sign = 'I'. <ls_rng>-option = 'BT'. <ls_rng>-low = '0100000001'. <ls_rng>-high = '0100000010'. ls_request_context_unit-technical_request-filter_select_options = lt_select_option. * Alternative Möglichkeit des Setzen Filter geht auch nicht * DATA lr_filter TYPE REF TO /iwbep/cl_mgw_req_filter. * CREATE OBJECT lr_filter TYPE /iwbep/cl_mgw_req_filter * EXPORTING * it_filter_select_options = lt_select_option * iv_filter_string = space. * ** Filter initialisieren * lr_filter->GET_CONVERSION_INFO( ). * lr_request_unittst->set_filter( lr_filter ). * DPC-Objekt init selbstgemacht lr_request_unittst = init_dp_for_unit_test( is_request_context = ls_request_context_unit ir_data_provider = mr_data_provider ir_odata_model = lr_odata_model ). TRY . * OData Query ausführen mr_data_provider->/iwbep/if_mgw_appl_srv_runtime~get_entityset( EXPORTING io_tech_request_context = lr_request_unittst IMPORTING er_entityset = DATA(lr_entityset) ). ASSIGN lr_entityset->* TO <lt_ent_businesspartner>. * Mit dem Filter müssen genau 10 Sätze zurückkommen cl_abap_unit_assert=>assert_equals( act = lines( <lt_ent_businesspartner> ) exp = 10 ). CATCH /iwbep/cx_mgw_base_exception INTO lr_exc_base. cl_abap_unit_assert=>fail( msg = lr_exc_base->get_text( ) ). ENDTRY. ENDMETHOD.
Wenn man die OData Calls vom Gateway Client als auch von dieser Testmethode einmal debugged, stellt man fest, dass das Problem tief im OData Framework in Methode /IWBEP/CL_MGW_FILTER_EXPRSN, GET_EXPRESSION_TREE liegt. Das Framework möchte die Filterparameter als Tabelle von Expressions bekommen. Die folgende Abbildung zeigt das benötigte Format. Dieses Format ist extrem kryptisch.
Fazit
ABAP Unit Test für die Testautomatisierung einzusetzen, ist generell sehr sinnvoll. Im Kontext eines OData Service halte ich die Testautomatisierung der DPC_EXT-Klasse wegen der aufgezeigten Problemen nicht für sinnvoll.
Alternative Ansätze zur Testautomatisierung eines OData Service sind:
- Auslagern der Logik aus der DPC_EXT-Klasse in Data Access Object Klassen: Diese lassen sich dann problemlos mit ABAP Unit Testen, weil sie keine Abhängigkeiten zum OData Framework haben
- End2End-Test über externe Tools wie SOAPUI oder einem OData Client wie pyodata. Pyodata lässt sich aus dem SAP GitHub beziehen und hat den Anspruch, ein Enterprise-ready Python OData client zu sein.
Hast du noch Fragen zu OData oder zu anderen Themen?
Nutze gerne unsere Kommentarfunktion
Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.
Mario sagt:
Den Inhalt der read_model()-Methode hast du vergessen. Ich habe dazu folgende Zeilen aus der Methode /IWBEP/CL_MGW_ABS_DATA->GET_MODEL rausgezogen, um eine fertig gefüllte Modelklasse zu bekommen:
———————————-
data lo_model type ref to /iwbep/if_mgw_odata_fw_model.
data(lo_metadata_provider) = /iwbep/cl_mgw_med_provider=>get_med_provider( ).
call function ‘/IWBEP/FM_MGW_MODEL_LOAD_SET’. “SAP Note 2479853++
lo_model ?= lo_metadata_provider->get_service_metadata( iv_internal_service_name = lv_service_name
iv_internal_service_version = lv_service_version ).
call function ‘/IWBEP/FM_MGW_MODEL_LOAD_RESET’. “SAP Note 2479853++
———————————-
Man schaue in die Transaktion “SEGW”, um die gewünschten Werte für lv_service_name und lv_service_version zu erhalten.
tar sagt:
Den Inhalt der read_model()-Methode hast du vergessen. Ich habe dazu folgende Zeilen aus der Methode /IWBEP/CL_MGW_ABS_DATA->GET_MODEL rausgezogen, um eine fertig gefüllte Modelklasse zu bekommen:
———————————-
data lo_model type ref to /iwbep/if_mgw_odata_fw_model.
data(lo_metadata_provider) = /iwbep/cl_mgw_med_provider=>get_med_provider( ).
call function ‘/IWBEP/FM_MGW_MODEL_LOAD_SET’. “SAP Note 2479853++
lo_model ?= lo_metadata_provider->get_service_metadata( iv_internal_service_name = lv_service_name
iv_internal_service_version = lv_service_version ).
call function ‘/IWBEP/FM_MGW_MODEL_LOAD_RESET’. “SAP Note 2479853++
———————————-
Man schaue in die Transaktion “SEGW”, um die gewünschten Werte für lv_service_name und lv_service_version zu erhalten.