Monday, 10 January 2022

ABAP Unit Tests: Generate a VALUE # Statement for the Contents of an internal Table

Creating test data for a meaningful ABAP unit test can be challenging. The data preview in the ABAP Development Tools (ADT) for Eclipse can help with that for database tables or views.

Let’s take table T100 (ABAP messages) as an example. First open the table definition and then click on the F8 button to trigger the data preview.

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
ABAP Data Explorer in Eclipse

When you right click within the Data Preview area, you get the context menu option:
‘Copy all rows as ABAP value statement’. If you don’t see this option, your ABAP Development Tools in Eclipse version might be outdated.

Choosing this options copies an ABAP VALUE # statement to the clipboard. This generated statement is used to populate an internal table with the structure of table T100.

Now you can add this statement into your unit test class, or here simply into a notepad application:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
VALUE # statement in notepad

This is great for database tables but often times you might also need the content of an internal table as an input for a method or function call.

To achieve this, we will create a little debugger script that can capture the content of an arbitrary internal table as a VALUE # statement.

First, create the following structure in the ABAP dictionary:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
SE11 view of structure ZDEBUGGER_SCRIPTING_S

Or simply copy and paste the following structure definition in the ADT structure editor:

@EndUserText.label : 'Fields for Use in Debugger Scripts'
@AbapCatalog.enhancement.category : #EXTENSIBLE_CHARACTER_NUMERIC
define structure zdebugger_scripting_s {
  itab             : ortabname;
  itab_from        : fsline;
  itab_to          : feline;
  all_table_fields : xfeld;

}

Next, go to transaction SAS to create the debugger script.

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification

Replace the coding in the editor with the following code:

*---------------------------------------------------------------------*
*       CLASS lcl_debugger_script DEFINITION
*---------------------------------------------------------------------*
*
*---------------------------------------------------------------------*
CLASS lcl_debugger_script DEFINITION INHERITING FROM  cl_tpda_script_class_super  .

  PUBLIC SECTION.
    DATA gv_tabname    TYPE tabname.
    DATA gv_tab_from   TYPE i.
    DATA gv_tab_to     TYPE i.
    DATA gt_content    TYPE tpda_scr_table_content_it.
    DATA gt_components TYPE tpda_scr_table_comp_it.
    DATA gv_all_fields TYPE xfeld.
    DATA gv_field_cnt  TYPE i.

    METHODS prologue  REDEFINITION.
    METHODS init      REDEFINITION.
    METHODS script    REDEFINITION.
    METHODS end       REDEFINITION.

    METHODS get_table_fields.
    METHODS build_val_pound_expression.

ENDCLASS.                    "lcl_debugger_script DEFINITION
*---------------------------------------------------------------------*
*       CLASS lcl_debugger_script IMPLEMENTATION
*---------------------------------------------------------------------*
*
*---------------------------------------------------------------------*
CLASS lcl_debugger_script IMPLEMENTATION.
  METHOD prologue.
*** generate abap_source (source handler for ABAP)
    super->prologue( ).
  ENDMETHOD.                    "prolog

  METHOD init.
    DATA lt_sval   TYPE STANDARD TABLE OF sval.
    DATA lv_answer TYPE c LENGTH 1.
    DATA lv_title  TYPE string.
    DATA ls_popup  TYPE ZDEBUGGER_SCRIPTING_S. "Just for the where used list

    IMPORT tabname TO gv_tabname FROM MEMORY ID sy-repid.

    lv_title = 'Script Control Parameters'(000).

    lt_sval = VALUE #( tabname = 'ZDEBUGGER_SCRIPTING_S'
                       ( fieldname = 'ITAB'             fieldtext = 'Internal Table Name'(001)  value = gv_tabname field_obl = abap_true )
                       ( fieldname = 'ALL_TABLE_FIELDS' fieldtext = 'Use all table fields'(002) value = abap_true                        )
                       ( fieldname = 'ITAB_FROM'        fieldtext = 'From record'(003)          value = '1'                              )
                       ( fieldname = 'ITAB_TO'          fieldtext = 'To record'(004)            value = '9999'                           )
                     ).

    CALL FUNCTION 'POPUP_GET_VALUES'
      EXPORTING
        popup_title     = lv_title
      IMPORTING
        returncode      = lv_answer
      TABLES
        fields          = lt_sval
      EXCEPTIONS
        error_in_fields = 1
        OTHERS          = 2.

    IF sy-subrc IS NOT INITIAL.
      raise_error( ).
    ENDIF.

    IF lv_answer = 'A'.
*     User canceled
      raise_error( ).
    ENDIF.

    LOOP AT lt_sval INTO DATA(ls_sval).
      CASE sy-tabix.
        WHEN 1.
          gv_tabname   = ls_sval-value.
          EXPORT tabname FROM gv_tabname TO MEMORY ID sy-repid.

        WHEN 2.
          gv_all_fields = ls_sval-value.

        WHEN 3.
          gv_tab_from  = ls_sval-value.

        WHEN 4.
          gv_tab_to    = ls_sval-value.

      ENDCASE.

    ENDLOOP.

  ENDMETHOD.                    "init
  METHOD script.
    DATA lo_tab     TYPE REF TO cl_tpda_script_tabledescr.
    DATA lt_val     TYPE tpda_scr_table_content_it.
    DATA lt_comp    TYPE tpda_scr_table_comp_it.
    DATA lv_records TYPE i.

    TRY.
        lo_tab ?= cl_tpda_script_data_descr=>factory( CONV #( gv_tabname ) ).

        lv_records = lo_tab->linecnt( ).

        IF lv_records < gv_tab_to.
          gv_tab_to = lv_records.
        ENDIF.

        lo_tab->content( EXPORTING p_line_from   = gv_tab_from
                                   p_line_to     = gv_tab_to
                         IMPORTING p_it_comp_val = gt_content ).

        gt_components = lo_tab->components( ).

        get_table_fields( ).

        build_val_pound_expression( ).

      CATCH cx_root INTO DATA(lo_root).
        RETURN.
    ENDTRY.

  ENDMETHOD.                    "script

  METHOD get_table_fields.
    DATA lt_sval   TYPE STANDARD TABLE OF spopli.
    DATA lv_answer TYPE c LENGTH 1.
    DATA lv_title  TYPE string.

    gv_field_cnt = lines( gt_components ).

    IF gv_all_fields = abap_true.
      RETURN.
    ENDIF.

    lv_title = 'Select Table Fields'(003).

    LOOP AT gt_components INTO DATA(ls_comp).
      APPEND VALUE #( selflag = abap_true varoption = ls_comp-compname ) TO lt_sval.
    ENDLOOP.

    CALL FUNCTION 'POPUP_TO_DECIDE_LIST'
      EXPORTING
        mark_flag          = abap_true
        mark_max           = 100
        textline1          = 'Fieldname'(005)
        titel              = 'Select Table Fields'(006)
      IMPORTING
        answer             = lv_answer
      TABLES
        t_spopli           = lt_sval
      EXCEPTIONS
        not_enough_answers = 1
        too_much_answers   = 2
        too_much_marks     = 3
        OTHERS             = 4.

    IF sy-subrc IS NOT INITIAL.
      RETURN.
    ENDIF.

    IF lv_answer = 'A'.
*     User canceled
      RETURN.
    ENDIF.

    CLEAR gv_field_cnt.

    LOOP AT lt_sval INTO DATA(ls_sval).
      IF ls_sval-selflag IS INITIAL.
        READ TABLE gt_components INDEX sy-tabix ASSIGNING FIELD-SYMBOL(<ls_comp>).
        CHECK sy-subrc IS INITIAL.

        CLEAR <ls_comp>-compname.
      ELSE.
        ADD 1 TO gv_field_cnt.
      ENDIF.

    ENDLOOP.

  ENDMETHOD.

  METHOD build_val_pound_expression.
    DATA lv_max_fields_per_line TYPE n LENGTH 2 VALUE '10'.
    DATA lv_max_field_length    TYPE i.

    DATA BEGIN OF ls_value.
    DATA record TYPE c LENGTH 255.
    DATA END OF ls_value.

    DATA lt_value LIKE STANDARD TABLE OF ls_value.

    LOOP AT gt_components INTO DATA(ls_comp).
      DATA(lv_length) = strlen( ls_comp-compname ).

      IF lv_length > lv_max_field_length.
        lv_max_field_length = lv_length.
      ENDIF.

    ENDLOOP.

    APPEND VALUE #( record = |{ gv_tabname CASE = LOWER } = VALUE #( | ) TO lt_value.

    LOOP AT gt_content ASSIGNING FIELD-SYMBOL(<ls_tabrecord>).
      IF gv_field_cnt > lv_max_fields_per_line.
        APPEND VALUE #( record  = '(' ) TO lt_value.
      ELSE.
        ls_value-record = '( '.
      ENDIF.

      LOOP AT <ls_tabrecord>-fields INTO DATA(lv_field).
        READ TABLE gt_components INDEX sy-tabix INTO ls_comp.
        CHECK sy-subrc IS INITIAL AND ls_comp-compname IS NOT INITIAL.

        CASE ls_comp-typid.
          WHEN 'D' OR 'T'.
            CHECK lv_field CN '0 '.

          WHEN 'I' OR 'N' OR 'P'.
            CHECK lv_field CN '0., '.

          WHEN 'C'.
            CHECK lv_field <> space.

        ENDCASE.

        IF gv_field_cnt > lv_max_fields_per_line.
          IF ls_comp-compname <> 'MANDT'.
            ls_value-record = | { ls_comp-compname WIDTH = lv_max_field_length } = '{ lv_field }' |.
          ELSE.
            ls_value-record = | { ls_comp-compname WIDTH = lv_max_field_length } = sy-mandt |.
          ENDIF.

          APPEND ls_value TO lt_value.
        ELSE.
          IF ls_comp-compname <> 'MANDT'.
            ls_value-record &&= | { ls_comp-compname } = '{ lv_field }' |.
          ELSE.
            ls_value-record &&= | { ls_comp-compname } = sy-mandt |.
          ENDIF.
        ENDIF.

      ENDLOOP.
      IF gv_field_cnt <= lv_max_fields_per_line
        AND ls_value IS NOT INITIAL.
        ls_value-record &&= ' )'.
        APPEND ls_value TO lt_value.
      ELSE.
        APPEND VALUE #( record = ')' ) TO lt_value.
      ENDIF.

      CLEAR ls_value.
    ENDLOOP.

    APPEND VALUE #( record = |).| ) TO lt_value.

    EDITOR-CALL FOR lt_value.

  ENDMETHOD.

  METHOD end.
*** insert your code which shall be executed at the end of the scripting (before trace is saved)
*** here

  ENDMETHOD.                    "end
ENDCLASS.                    "lcl_debugger_script IMPLEMENTATION​

Save the debugger script under a new name:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Save Debugger Script

Make sure that the ‘Trigger’ for the script is set to ‘Execute Directly’.

Let’s test the script with a little sample program:

REPORT z_test_value_pound_script.

START-OF-SELECTION.
  DATA lt_t100 TYPE TABLE OF t100.

  SELECT FROM t100
  FIELDS *
  WHERE sprsl = @sy-langu
  ORDER BY arbgb, msgnr
  INTO TABLE @lt_t100
  UP TO 1000 ROWS.

  BREAK-POINT.

Execute the report.

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Test report with data loaded from table T100

Switch to the ‘Script’ tab and load the newly created script:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Load Script ZRSTPDA_SCRIPT_VALUE_POUND

Start the script:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Start Script

The script will send a pop up window where you can specify the internal table name, here LT_T100, whether you want to use all of the fields from the table (Use all table fields = ‘X’ ) and which rows from the internal table you want to use.

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Editor display of generated VALUE # statement

Now you can copy and paste the generated statement into your unit test class.

If you don’t need all the fields from the table, then deselect the ‘Use all table fields’ indicator.

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Do not want all the table fields and only 5 rows

In this case you get an extra pop up to select, which fields from the table you actually want:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Field Selection

And here the result:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification
Restricted Result Set

The script should work fine for internal tables with a flat structure. I haven’t tested it with deep, nested structures.

In SAP S/4HANA on premise release 2022 this debugger script will be delivered under the name RSTPDA_SCRIPT_VALUE_POUND.

Example of test data in a unit test class generated with this script:

SAP ABAP Unit Tests, SAP ABAP Development, SAP ABAP Exam Prep, SAP ABAP Career, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Skills, SAP ABAP Certification

Generated data used in a test class method

Have fun generating test data for your unit tests with this little tool.

Source: sap.com

No comments:

Post a Comment