ABAP Unit - OData testen
In diesem Artikel geht es um die Testbarkeit von OData Services und wie du damit auch Schnittstellen einfach und schnell testen kannst.
Inhaltsverzeichnis
In diesem Artikel werden wir uns das Testen eines OData Service auf zwei Arten ansehen. Einmal über den klassischen Weg mit einer Data-Provider Klasse und einmal über den neuen Weg eines Service über RAP.
Data-Provider Klasse
Allgemein
Um eine Data-Provider Klasse testen zu können, müssen wir zuerst eine Instanz der Klasse, sowie eine Instanz des Kontext-Objekts erzeugen, um überhaupt sauber die Methode testen zu können. Hierfür sind einige Zusatzschritte nötig, damit wir den Provider sauber testen können. Dazu wollen wir dir im ersten Schritt einmal den Testfall etwas näherbringen.
Wir möchten in unserem Test einen OData Service für die folgende Tabelle zur Verfügung stellen, der die typischen CRUD Operationen zur Verfügung stellt. Die Tabelle ist mit allen möglichen Demo-Felder bestückt:
Das Legen wir einen Service über die Transaktion SEGW an und importieren die Tabelle als Struktur. Das System wird einen entsprechenden Vorschlag für die Namen der Felder machen. Die Entität benennen wir der Einfachheit “DATA”.
Die vollständige Implementierung der DPC-Klasse ZCL_TEST_UNITTEST_DPC_EXT findest du bei den entsprechenden Beispielen am Ende des Buches. Dazu nun die folgende Testklasse, um den Testfall für das Anlegen und Lesen zu implementieren.
CLASS ltc_odata DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA:
mo_cut TYPE REF TO zcl_test_unittest_dpc_ext,
mo_request_context_object TYPE REF TO /iwbep/cl_mgw_request_unittst.
METHODS:
read_data FOR TESTING RAISING cx_static_check,
create_data FOR TESTING RAISING cx_static_check,
prepare_cut.
ENDCLASS.
CLASS ltc_odata IMPLEMENTATION.
METHOD prepare_cut.
mo_cut = NEW zcl_test_unittest_dpc_ext( ).
DATA(ls_request_context) = VALUE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit(
technical_request = VALUE #(
source_entity_set = 'DATASet'
target_entity_set = 'DATASet'
source_entity_type = 'DATA'
target_entity_type = 'DATA'
)
).
mo_request_context_object = mo_cut->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( ls_request_context ).
ENDMETHOD.
METHOD read_data.
DATA:
lt_data TYPE STANDARD TABLE OF zbs_test_data.
prepare_cut( ).
DATA(lt_filter) = VALUE /iwbep/t_mgw_select_option(
(
property = 'CURRENCY'
select_options = VALUE #( ( sign = 'I' option = 'EQ' low = 'CHF' ) )
)
).
mo_cut->/iwbep/if_mgw_appl_srv_runtime~get_entityset(
EXPORTING
io_tech_request_context = mo_request_context_object
it_filter_select_options = lt_filter
IMPORTING
er_entityset = DATA(lr_entity)
es_response_context = DATA(ls_response_context)
).
ASSIGN lr_entity->* TO FIELD-SYMBOL(<lt_data>).
lt_data = CORRESPONDING #( <lt_data> ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_data ) exp = 2 ).
ENDMETHOD.
METHOD create_data.
DATA:
ls_new TYPE zbs_test_data.
prepare_cut( ).
DATA(ls_new_data) = VALUE zbs_test_data(
description = 'A set of testdata from UNIT Test'
amount = '12.50'
currency = 'USD'
creation_user = sy-uname
creation_time = sy-uzeit
creation_data = sy-datum
).
DATA(lo_data_provider) = NEW /iwbep/cl_cp_v2_entry_provider( REF #( ls_new_data ) ).
mo_cut->/iwbep/if_mgw_appl_srv_runtime~create_entity(
EXPORTING
io_data_provider = lo_data_provider
io_tech_request_context = mo_request_context_object
IMPORTING
er_entity = DATA(lr_entity)
).
ASSIGN lr_entity->* TO FIELD-SYMBOL(<ls_data>).
ls_new = CORRESPONDING #( <ls_data> ).
cl_abap_unit_assert=>assert_not_initial( ls_new-identifier ).
ENDMETHOD.
ENDCLASS.
Ablauf
Wie sieht es nun mit dem eigentlichen Testfall und der Ausführung aus? Für den Aufbau der Testinstanz müssen einige Schritte vorher ausgeführt werden, da wir auch noch ein Kontext Objekt benötigen, um die Methoden ausführen zu können.
- Vorbereitung
- Kontext-Struktur befüllen mit zu testender Entität und dem Namen des Sets
- Init-Methode der Service-Runtime aufrufen
- Sichern des Kontext-Objekts in der Testklasse
- Lesen von Daten
- Definition eines Filters zur Einschränkung der Datensätze
- Aufruf der Methode GET_ENTITYSET mit Kontext Objekt und Zusatzdaten (Filter)
- Prüfung der gefundenen Datensätze für den Test
- Anlegen von Daten
- Vorbereitung eines neuen Datensatzes
- Erstellung eines Datenprovider-Objekts mit den Daten als Referenz
- Aufruf der Methode CREATE_ENTITY mit Kontext Objekt und Datenprovider
- Prüfung des neuen Schlüssels für den Test
Hinweis: Das Testen von OData Services über diese Methode wird von uns nur teilweise empfohlen, da die Methodik nicht den vollen Funktionsumfang der Services unterstützt. Wenn möglich, sollte auf das Testen mit Service Bindings zurückgegriffen werden.
Service Binding
Das Service Bindung ist die neueste Variante einen OData zur Verfügung zu stellen und zwar aus der Implementierung eines Business Objekt über RAP. Dabei stellt Eclipse einen Wizard zur Verfügung der das Coding in einer eigenen Testklasse implementiert oder du schreibst dir deine Tests selbst. Wir möchten dir an dieser Stelle nicht zeigen, wie du per RAP ein eigenes Business Objekt aufbaust, aber wie du aus dem fertigen Service eine Testklasse generierst und diese dann testest. RAP für sich, würde auch wieder ein halbes Buch befüllen.
Vorbereitung
Um die Testklasse automatisch über den Wizard anlegen zu können, musst du das Service-Bindung des Datenmodells öffnen, für die du deine Testfälle definieren möchtest. Das Objekt erstellt für einen Service den entsprechenden Endpunkt und das Protokoll (in diesem Fall OData v2) zur Verfügung.
Mit einem Rechtsklick auf die entsprechende Entität kannst du dann über das Kontextmenü ein Testklasse generieren.
Im Wizard musst du dann nur noch das Paket und den Namen der Testklasse hinterlegen, der Rest wird automatisch vorbelegt. Am Ende wird eine globale Testklasse angelegt, während die eigentlichen Testfälle wieder in den lokalen Testklassen dieser Klasse liegen.
Beispiel
Wie schon beim anderen OData-Service implementieren wir beispielhaft für dich den Fall für das Lesen von Datensätzen und die Anlage eines neuen Datensatzes. Die Handhabung des lokalen Proxys ist um einiges einfacher, als das Arbeiten über die DPC Klasse. Für den Proxy gibt es jeweils eine Version für On-Premise und eine für die BTP, je nachdem auf welcher Umgebung du unterwegs bist.
CLASS ltc_odata_service DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA:
mo_client_proxy TYPE REF TO /iwbep/if_cp_client_proxy.
METHODS:
setup RAISING cx_static_check,
create_data FOR TESTING RAISING cx_static_check,
read_data FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_odata_service IMPLEMENTATION.
METHOD setup.
" On-Premise Proxy
mo_client_proxy = /iwbep/cl_cp_client_proxy_fact=>create_v2_local_proxy(
VALUE #( service_id = 'ZBS_UI_DEMOSIMPLERAP_O2' service_version = '0001' )
).
" BTP Proxy
* mo_client_proxy = cl_web_odata_client_factory=>create_v2_local_proxy(
* VALUE #( service_id = 'ZBS_UI_DEMOSIMPLERAP_O2' service_version = '0001' )
* ).
ENDMETHOD.
METHOD create_data.
DATA:
ls_business_data TYPE ZBS_C_DemoSimpleRAP,
lo_request TYPE REF TO /iwbep/if_cp_request_create,
lo_response TYPE REF TO /iwbep/if_cp_response_create.
ls_business_data = VALUE #(
Item = 'HAMMER'
Description = 'A good tool for nails'
ItemNumber = 4911
).
lo_request = mo_client_proxy->create_resource_for_entity_set( 'SimpleRap' )->create_request_for_create( ).
lo_request->set_business_data( ls_business_data ).
lo_response = lo_request->execute( ).
CLEAR ls_business_data.
lo_response->get_business_data( IMPORTING es_business_data = ls_business_data ).
cl_abap_unit_assert=>assert_not_initial( ls_business_data-UniqueKey ).
ENDMETHOD.
METHOD read_data.
DATA:
lt_business_data TYPE TABLE OF ZBS_C_DemoSimpleRAP,
lo_request TYPE REF TO /iwbep/if_cp_request_read_list,
lo_response TYPE REF TO /iwbep/if_cp_response_read_lst,
lo_filter_factory TYPE REF TO /iwbep/if_cp_filter_factory,
lt_r_number TYPE RANGE OF ZBS_C_DemoSimpleRAP-ItemNumber.
lo_request = mo_client_proxy->create_resource_for_entity_set( 'SimpleRap' )->create_request_for_read( ).
lt_r_number = VALUE #( ( sign = 'I' option = 'EQ' low = 2 ) ).
lo_filter_factory = lo_request->create_filter_factory( ).
DATA(lo_filter_node) = lo_filter_factory->create_by_range(
iv_property_path = 'ITEMNUMBER'
it_range = lt_r_number ).
lo_request->set_filter( lo_filter_node ).
lo_request->set_top( 50 )->set_skip( 0 ).
lo_response = lo_request->execute( ).
lo_response->get_business_data( IMPORTING et_business_data = lt_business_data ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_business_data ) exp = 2 ).
ENDMETHOD.
ENDCLASS.
Für die Zuweisung der Daten verwenden wir den Consumption View (Core Data Service), da hier die entsprechenden Feldnamen für den OData Service bereitgestellt werden und für die Datenbank andere Feldnamen gelten könnten.
Ablauf
Den entsprechenden Ablauf in der Testklasse noch einmal kurz erklärt:
- Vorbereitung
- Erstellung des lokalen Proxy Objekts für die Verbindung zum Service
- Lesen von Daten
- Erzeugen des Request Objekts zum Lesen
- Setzen des Filters und TOP/SKIP Werte
- Ausführen des Requests
- Lesen der Daten aus dem Response Objekt
- Anlegen von Daten
- Erzeugen des neuen Datensatzes
- Erzeugen des Request Objekts zur Anlage
- Übergabe der Daten
- Ausführen des Requests
- Auswerten der Antwort (Befüllung des Schlüssels)
Test Double
Du möchtest nicht gegen die echten Daten in der Datenbank testen? Dann kannst du auch einen Test Double für den Core Data Service verwenden, der angesprochen wird. Der SQL Double für die Tabelle wird hier nicht funktionieren.
Dazu einfach in der Setup Methode das entsprechende Double vorbereiten und aktivieren. Die Zugriffe über den Proxy laufen dann gegen den CDS Double.
Fazit
OData Services können genau so wie andere Objekte getestet und geprüft werden, was deine Entwicklung in Zukunft stabiler machen sollte. Die Bereitstellung und Weiterentwicklung solcher Services wird in Zukunft einen großen Teil der Ressourcen ausmachen, die im Backend benötigt werden.