ABAP Unit - Testable Code (Part 1)
In this article, we'll look at how you can cleanly implement new functions in older code and then test it.
Table of contents
What does writing testable code mean? In this chapter we want to go into some methods and how you can modify or expand your existing coding accordingly so that it can be tested.
General
In general, we speak of testable code if it can be checked automatically with ABAP unit tests. So there is a clean interface to a method or a subroutine and there are as few dependencies as possible on global data, i.e. a clean encapsulation of the code.
Island of Happiness
Methodology
Describes a method for extending legacy code by adding logic or functionality. In the following example we have legacy code that should be expanded with new functionality. In such a case, the new logic can be developed as a separate class, decoupled from the main code. The relevant data and logics are transferred via methods of the new class. The new logic is thus encapsulated in a global class and can be tested automatically with ABAP unit tests. All of our new logic can be neatly developed and encapsulated and we hardly need to adapt the existing code.
Example
The report was developed classically without FORM routines and still contains a lot of global variables and interdependencies. Our new implementation is intended to add new functions to the report, in which we want to enrich the output data with additional geodata. We would like to provide unit tests for the new functionality.
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( ).
We implement the new function in a global class. Accordingly, we provide an interface that provides the interface to the report. Furthermore, we need the output structure for the interface to the methods, which we then have to announce accordingly. We adopt the structure in the interface and replace the definition in the report. Our interface then looks like this:
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.
The report is now enriched with our new logic in the appropriate places in order to work with minimal changes.
- Change of the declaration part
- Instantiation of the object
- Calling the additional logic
At the end, we still implement the logic in the class and ABAP unit tests in order to make our written code automatically testable and to be able to carry out future extensions more easily. You can find the example implementation of the class in the examples at the end of the book.
Final class
You can find the example implementation of the class here at this point, so that you can encapsulate the new functions neatly:
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.
Conclusion
You can see from our example that even in old code, new code can be implemented cleanly. You can then cleanly test and validate these new functions with ABAP Unit.