ABAP Unit - Testbarer Code (Teil 1)
In diesem Artikel schauen wir uns an, wie du auch in älterem Code sauber neue Funktionen implementieren und du diese im Anschluss auch testen kannst.
Inhaltsverzeichnis
Was bedeutet testbaren Code zu schreiben? In diesem Artikel wollen wir auf einige Methoden eingehen und wie du dein bestehendes Coding entsprechend umbauen oder erweitern kannst, damit er testbar wird.
Allgemein
Im Allgemeinen sprechen wir von testbarem Code, wenn dieser automatisch mit ABAP Unit Tests geprüft werden kann. Also eine saubere Schnittstelle zu einer Methode oder einem Unterprogramm existiert und so wenig wie möglich Abhängigkeiten zu globalen Daten bestehen, also einer sauberen Kapselung des Codes.
Island of Happiness
Methodik
Beschreibt eine Methode zur Erweiterung von Legacy Code durch eine Erweiterung von Logik oder Funktionen. Im folgenden Beispiel haben wir Legacy Code, der durch eine neue Funktionalität erweitert werden soll. In solch einem Fall kann die neue Logik als eigene Klasse entkoppelt vom Hauptcode entwickelt werden. Die entsprechend wichtigen Daten und Logiken werden über Methoden der neuen Klasse übergeben. Damit ist die neue Logik in einer globalen Klasse gekapselt und kann mit ABAP Unit Tests automatisiert getestet werden. All unsere neue Logik kann sauber entwickelt und gekapselt werden und wir müssen den bestehenden Code kaum anpassen.
Beispiel
Der Report wurde klassisch ohne FORM Routinen entwickelt und enthält noch sehr viele globale Variablen und Abhängigkeiten untereinander. Unsere neue Implementierung soll den Report um neue Funktionen erweitern, bei der wir die Ausgabedaten noch um zusätzliche Geodaten anreichern wollen. Wir möchten dabei Unit Tests für die neue Funktionalität zur Verfügung stellen.
REPORT z_test_ioh.
*----------------------------------------------------------------------*
*--- Globale data
*----------------------------------------------------------------------*
TABLES:
t001.
TYPES:
BEGIN OF ts_output,
bukrs TYPE t001-bukrs,
butxt TYPE t001-butxt,
name1 TYPE adrc-name1,
name2 TYPE adrc-name2,
city1 TYPE adrc-city1,
addr_group TYPE adrc-addr_group,
END OF ts_output,
tt_output TYPE STANDARD TABLE OF ts_output WITH EMPTY KEY.
DATA:
gt_company_code TYPE SORTED TABLE OF t001 WITH UNIQUE KEY bukrs,
gs_company_code TYPE t001,
gs_address TYPE adrc,
gt_output_table TYPE tt_output,
gs_output_table TYPE ts_output,
gs_vari TYPE disvariant,
go_alv TYPE REF TO cl_salv_table.
*----------------------------------------------------------------------*
*--- Selection screen
*----------------------------------------------------------------------*
SELECTION-SCREEN BEGIN OF BLOCK b01.
SELECT-OPTIONS:
s_bukrs FOR t001-bukrs,
s_waers FOR t001-waers.
PARAMETERS:
p_vari TYPE slis_vari DEFAULT '/DEFAULT',
p_test AS CHECKBOX DEFAULT abap_true.
SELECTION-SCREEN END OF BLOCK b01.
*----------------------------------------------------------------------*
*--- Events
*----------------------------------------------------------------------*
INITIALIZATION.
AUTHORITY-CHECK OBJECT 'S_TCODE'
ID 'TCD' FIELD 'Z60DUMMY_TCODE'.
IF sy-subrc <> 0.
MESSAGE e000(z60bc) WITH 'No authority for transaction' 'Z60DUMMY_TCODE'.
ENDIF.
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_vari.
gs_vari-report = sy-repid.
gs_vari-username = sy-uname.
gs_vari-handle = 'HDL'.
CALL FUNCTION 'REUSE_ALV_VARIANT_F4'
EXPORTING
is_variant = gs_vari
i_save = 'A'
IMPORTING
es_variant = gs_vari
EXCEPTIONS
OTHERS = 1.
IF sy-subrc = 0.
p_vari = gs_vari-variant.
ENDIF.
START-OF-SELECTION.
CLEAR: gt_company_code, gt_output_table.
SELECT *
FROM t001
WHERE bukrs IN @s_bukrs
AND waers IN @s_waers
INTO TABLE @gt_company_code.
LOOP AT gt_company_code INTO gs_company_code.
CLEAR: gs_output_table.
gs_output_table = CORRESPONDING #( gs_company_code ).
SELECT SINGLE *
FROM adrc
WHERE addrnumber = @gs_company_code-adrnr
INTO @gs_address.
IF sy-subrc = 0.
gs_output_table = CORRESPONDING #( BASE ( gs_output_table ) gs_address ).
ENDIF.
INSERT gs_output_table INTO TABLE gt_output_table.
ENDLOOP.
cl_salv_table=>factory(
IMPORTING r_salv_table = go_alv
CHANGING t_table = gt_output_table ).
DATA(lo_func) = go_alv->get_functions( ).
lo_func->set_all( ).
DATA(lo_disp) = go_alv->get_display_settings( ).
lo_disp->set_striped_pattern( abap_true ).
lo_disp->set_list_header( 'Found data' ).
DATA(lo_lay) = go_alv->get_layout( ).
lo_lay->set_default( abap_true ).
lo_lay->set_key( VALUE #( report = sy-repid handle = 'HDL' ) ).
lo_lay->set_save_restriction( ).
IF p_vari IS NOT INITIAL.
lo_lay->set_initial_layout( p_vari ).
ENDIF.
go_alv->display( ).
Die neue Funktion implementieren wir in einer globalen Klasse. Entsprechend stellen wir ein Interface zur Verfügung, welches die Schnittstelle zum Report bereitstellt. Weiterhin benötigen wir für die Schnittstelle zu den Methoden die Ausgabestruktur, die wir dann entsprechend bekannt geben müssen. Die Struktur übernehmen wir mit ins Interface und ersetzen die Definition im Report. Entsprechend sieht unsere Schnittstelle dann wie folgt aus:
INTERFACE zif_test_ioh PUBLIC.
TYPES:
td_geometry_data TYPE p LENGTH 15 DECIMALS 8,
BEGIN OF ts_output,
bukrs TYPE t001-bukrs,
butxt TYPE t001-butxt,
name1 TYPE adrc-name1,
name2 TYPE adrc-name2,
city1 TYPE adrc-city1,
addr_group TYPE adrc-addr_group,
" New Fields
latitude TYPE td_geometry_data,
longitude TYPE td_geometry_data,
END OF ts_output,
tt_output TYPE STANDARD TABLE OF ts_output WITH EMPTY KEY,
BEGIN OF ts_geometry,
latitude TYPE td_geometry_data,
longitude TYPE td_geometry_data,
END OF ts_geometry.
METHODS:
enrich_data
CHANGING
cs_output TYPE ts_output.
ENDINTERFACE.
Der Report wird nun an den entsprechenden Stellen mit unserer neuen Logik angereichert, um so mit minimalen Änderungen zu arbeiten.
- Änderung des Deklarationsteils
- Instanziierung des Objekts
- Aufruf der Zusatzlogik
Am Ende implementieren wir noch die Logik in der Klasse und ABAP Unit Tests, um unseren geschriebenen Code automatisiert testbar zu machen und zukünftige Erweiterungen einfacher durchführen zu können.
Abschluss
Die Beispielimplementierung der Klasse findest du hier an dieser Stelle, damit kannst du die neuen Funktionen sauber kapseln:
CLASS zcl_test_ioh DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_test_ioh.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS:
get_geolocation_for_city
IMPORTING
id_city TYPE adrc-city1
RETURNING VALUE(rs_geometry) TYPE zif_60bs_test_ioh=>ts_geometry.
ENDCLASS.
CLASS zcl_test_ioh IMPLEMENTATION.
METHOD zif_60bs_test_ioh~enrich_data.
DATA(ls_geometry) = get_geolocation_for_city( cs_output-city1 ).
IF ls_geometry IS INITIAL.
RETURN.
ENDIF.
cs_output-latitude = ls_geometry-latitude.
cs_output-longitude = ls_geometry-longitude.
ENDMETHOD.
METHOD get_geolocation_for_city.
" Call geolocation api
ENDMETHOD.
ENDCLASS.
Fazit
An unserem Beispiel erkennst du, dass selbst in altem Code sauber neuer Code implementiert werden kann. Im Anschluss kannst du diese neuen Funktionen noch sauber mit ABAP Unit testen und validieren.