Friday, 13 March 2020

ABAP unit: CL_ABAP_UNIT_ASSERT=>ASSERT_EQUALS method improved for asserting Standard tables

The ABAP class CL_ABAP_UNIT_ASSERT is a great class for asserting Actual and Expected data for ABAP unit testing. Asserting equal is comparing the Actual data with the Expected data that it is equal. If it is not equal than a failure will be raised.

The method ASSERT_EQUALS even supports deep structures.

As you can see in my blog ABAP BAPI BO Class Generator in paragraph “Testing the read method” I use deep structures a lot, for example for GET_DATA methods which return all BAPI data in a deep structures returning parameter, so I can use one returning parameter instead of multiple exporting parameters. Example:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

This deep structure data can be used in unit testing.

Where the standard SAP ASSERT_EQUALS method a little bit lacks, is on asserting Standard tables.  (CL_ABAP_UNIT_ASSERT version – SAP ABA version: 75D, SP: 2)

For non-ABAP developers: a standard table is a kind of internal memory / parameter table. These Standard tables are very important because almost all BAPIs make use of Standard tables.

I will first explain where it lacks and after that I will show you my solution.

Problem


To show the lacking of asserting Standard tables I created a test program with a deep structure type containing a hashed table, a sorted table and a standard table. And I created a deep structure variable for the Expected data and the Actual data filling these tables all with one line.

The types used in the test program:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

The content from Actual (act) and Expected (exp) differ on field “FIELD_2”. See picture below:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

For all the code of the test program, see “Appendix: ZZUT_ASSERT_EQ_STANDARD_TABLE”.

The result of the unit test (Ctrl + Shift + F10) is:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

See that component TEST_HASHED_TABLE shows exactly the difference at index 1 in field FIELD_2.

See that component TEST_SORTED_TABLE shows also exactly the difference at index 1 in field FIELD_2.

See that component TEST_STANDARD_TABLE does not show the difference. It shows that the Actual table has a line which is not in the Expected table and visa versa.

By comparing the content of the both tables…

◉ [[AAAA|Test value field 1|Test value field 2]]
◉ [[AAAA|Test value field 1|Test value field 2 modified.]]

…the field values are separated by a pipe, so we can conclude it must be the 3rd field, which is FIELD_2.

However BAPI tables can contain often much more fields. For example structure BAPI_ALM_ORDER_OPERATION contains 120 fields and than comparing data in this way is not possible anymore.

Solution


I created a new class ZCL_ABAP_UNIT_ASSERT to solve the problem. See “Appendix ZCL_ABAP_UNIT_ASSERT” for the ABAP code.

This class compares standard tables in more detail.

The functionality is:

◉ It searches through the data (can be a deep structure) looking for all Standard tables.
◉ It first sorts the Actual and Expected table.
◉ Than it compares the number of lines. If they differ, than it will be show as an error.
◉ Than it checks the Actual content with the Expected content line by line up to a maximum of 10 differences.
◉ Than it clears Actual table and Expected table, so these won’t be checked anymore by the standard SAP ASSERT_EQUALS.

The result using method ZCL_ABAP_UNIT_ASSERT=>ASSERT_EQUALS is:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

The messages for the standard table are not generated in the same failure as the one generated by the standard SAP ASSERT_EQUALS. So, now two failures are generated.

The first failure is for the Standard table by the custom ASSERT_EQUALS method.
The second failure is for the Hashed and Sorted table created by the standard SAP ASSERT_EQUALS method.

Failure 1: Critical Assertion Error: ‘Standard table: TEST_STANDARD_TABLE, index: 1, field: FIELD_2’

See that field TEST_STANDARD_TABLE has at row index 1 in field FIELD_2 a difference.

Remark: the row index is based on sorting the Standard table first.

The value difference is shown in the messages part:

Expected [Test value field 2 modified.] Actual [Test value field 2]

Failure 2: Critical Assertion Error: ‘Sc001_Test_Assert_Equals: ASSERT_EQUALS’

I selected now the second failure:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

You can see that the line related to component TEST_STANDARD_TABLE…

“> Component [TEST_STANDARD_TABLE]: Table Type [S-Table[1×128] of  … “

…is missing, because the Standard tables are now first checked by the custom ASSERT_EQUALS method and the result of that check is shown in the first failure.

Example of a line count difference.

I added one extra line to the Expected table of the Standard table. Now the test result is:

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

See failure is “…Table line count of … are not equal.’.
Expected table contains 2 lines, Actual table contains 1 line.

Example of more than 10 field differences

I added 15 more fields with all values differing from Actual versus Expected.

SAP ABAP Tutorial and Materials, SAP ABAP Learning, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

As you can see, for every differences a new failure is created up to a maximum of 10 differences. I maximized the number of differences to overcome to show too much differences.

On example field difference is FIELD_4. It expected is empty, but it actual contains 4.

Appendix: ZZUT_ASSERT_EQ_STANDARD_TABLE


REPORT ZZUT_ASSERT_EQ_STANDARD_TABLE.

CLASS unit_test_class DEFINITION FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.

    METHODS: sc001_test_assert_equals FOR TESTING.
ENDCLASS.       "unit_Test_Class


CLASS unit_test_class IMPLEMENTATION.

  METHOD sc001_test_assert_equals.

    TYPES:
      BEGIN OF test_record_type,
        key_field TYPE c LENGTH 4,
        field_1   TYPE c LENGTH 20,
        field_2   TYPE c LENGTH 40,
      END OF test_record_type.

    TYPES:
      BEGIN OF test_data_set_type,
        test_hashed_table   TYPE HASHED TABLE OF test_record_type
          WITH UNIQUE KEY key_field,
        test_sorted_table   TYPE SORTED TABLE OF test_record_type
          WITH UNIQUE KEY key_field,
        test_standard_table TYPE STANDARD TABLE OF test_record_type
          WITH DEFAULT KEY,
      END OF test_data_set_type.

    "Fill test data
    DATA(act_test_record_1) = VALUE test_record_type(
      key_field = 'AAAA'
      field_1   = 'Test value field 1'
      field_2   = 'Test value field 2'
    ).

    DATA(exp_test_record_1) = VALUE test_record_type(
      key_field = act_test_record_1-key_field
      field_1   = act_test_record_1-field_1
      field_2   = act_test_record_1-field_2 && | modified.|
    ).

    DATA act_test_data_set TYPE test_data_set_type.
    DATA exp_test_data_set TYPE test_data_set_type.

    INSERT act_test_record_1 INTO TABLE act_test_data_set-test_hashed_table.
    INSERT act_test_record_1 INTO TABLE act_test_data_set-test_sorted_table.
    APPEND act_test_record_1 TO act_test_data_set-test_standard_table.

    INSERT exp_test_record_1 INTO TABLE exp_test_data_set-test_hashed_table.
    INSERT exp_test_record_1 INTO TABLE exp_test_data_set-test_sorted_table.
    APPEND exp_test_record_1 TO exp_test_data_set-test_standard_table.

    cl_abap_unit_assert=>assert_equals(
        act = act_test_data_set
        exp = exp_test_data_set ).

  ENDMETHOD.

ENDCLASS.

Appendix: ZCL_ABAP_UNIT_ASSERT


CLASS zcl_abap_unit_assert DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    CLASS-METHODS assert_equals
      IMPORTING field_name              TYPE char30 OPTIONAL
                VALUE(act)              TYPE any
                VALUE(exp)              TYPE any
                ignore_hash_sequence    TYPE abap_bool DEFAULT abap_false
                tol                     TYPE f OPTIONAL
                msg                     TYPE csequence OPTIONAL
                level                   TYPE int1 DEFAULT if_aunit_constants=>severity-medium
                quit                    TYPE int1 DEFAULT if_aunit_constants=>method
                skip_default_assert     TYPE abap_bool DEFAULT abap_false
      RETURNING VALUE(assertion_failed) TYPE abap_bool.

  PROTECTED SECTION.

    CLASS-METHODS assert_equals_by_ref
      IMPORTING field_name              TYPE char30 OPTIONAL
                ignore_hash_sequence    TYPE abap_bool
                tol                     TYPE f
                msg                     TYPE csequence
                level                   TYPE int1
                quit                    TYPE int1
                skip_default_assert     TYPE abap_bool
      CHANGING  act                     TYPE any
                exp                     TYPE any
      RETURNING VALUE(assertion_failed) TYPE abap_bool.

    CLASS-METHODS assert_standard_table
      IMPORTING iv_clear_table_ind      TYPE abap_bool
                iv_table_name           TYPE typename
                iv_quit                 TYPE int1
      CHANGING  ct_act_table            TYPE STANDARD TABLE
                ct_exp_table            TYPE STANDARD TABLE
      RETURNING VALUE(rv_assert_failed) TYPE abap_bool.

ENDCLASS.



CLASS ZCL_ABAP_UNIT_ASSERT IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_ABAP_UNIT_ASSERT=>ASSERT_EQUALS
* +-------------------------------------------------------------------------------------------------+
* | [--->] FIELD_NAME                     TYPE        CHAR30(optional)
* | [--->] ACT                            TYPE        ANY
* | [--->] EXP                            TYPE        ANY
* | [--->] IGNORE_HASH_SEQUENCE           TYPE        ABAP_BOOL (default =ABAP_FALSE)
* | [--->] TOL                            TYPE        F(optional)
* | [--->] MSG                            TYPE        CSEQUENCE(optional)
* | [--->] LEVEL                          TYPE        INT1 (default =IF_AUNIT_CONSTANTS=>SEVERITY-MEDIUM)
* | [--->] QUIT                           TYPE        INT1 (default =IF_AUNIT_CONSTANTS=>METHOD)
* | [--->] SKIP_DEFAULT_ASSERT            TYPE        ABAP_BOOL (default =ABAP_FALSE)
* | [<-()] ASSERTION_FAILED               TYPE        ABAP_BOOL
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD assert_equals.

    "Pass act and exp by changing
    assertion_failed =
      assert_equals_by_ref(
        EXPORTING field_name              = field_name
                  ignore_hash_sequence    = ignore_hash_sequence
                  tol                     = tol
                  msg                     = msg
                  level                   = level
                  quit                    = if_aunit_constants=>no
                  skip_default_assert     = skip_default_assert
        CHANGING act                      = act
                 exp                      = exp ).

    "Handling quit
    CASE quit.
*      when if_Aunit_Constants=>program.
*        raise exception type cx_Aunit_Sbx_Quit_Test_Prog.
      WHEN if_aunit_constants=>class.
        RAISE EXCEPTION TYPE cx_aunit_sbx_quit_test_class.
      WHEN if_aunit_constants=>method.
        RAISE EXCEPTION TYPE cx_aunit_sbx_quit_test_method.
      WHEN if_aunit_constants=>quit-no.
        "
      WHEN OTHERS.
        RAISE EXCEPTION TYPE cx_aunit_sbx_quit_test_method.
    ENDCASE.

  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Protected Method ZCL_ABAP_UNIT_ASSERT=>ASSERT_EQUALS_BY_REF
* +-------------------------------------------------------------------------------------------------+
* | [--->] FIELD_NAME                     TYPE        CHAR30(optional)
* | [--->] IGNORE_HASH_SEQUENCE           TYPE        ABAP_BOOL
* | [--->] TOL                            TYPE        F
* | [--->] MSG                            TYPE        CSEQUENCE
* | [--->] LEVEL                          TYPE        INT1
* | [--->] QUIT                           TYPE        INT1
* | [--->] SKIP_DEFAULT_ASSERT            TYPE        ABAP_BOOL
* | [<-->] ACT                            TYPE        ANY
* | [<-->] EXP                            TYPE        ANY
* | [<-()] ASSERTION_FAILED               TYPE        ABAP_BOOL
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD assert_equals_by_ref.

    DATA(lr_act_type_descr) = cl_abap_typedescr=>describe_by_data( act ).

    CASE lr_act_type_descr->kind.

      WHEN cl_abap_typedescr=>kind_table.

        DATA(lr_act_table_descr) = CAST cl_abap_tabledescr( lr_act_type_descr ).

        CASE lr_act_table_descr->table_kind.

          WHEN cl_abap_tabledescr=>tablekind_std.

            FIELD-SYMBOLS <act_standard_table> TYPE STANDARD TABLE.
            FIELD-SYMBOLS <exp_standard_table> TYPE STANDARD TABLE.

            ASSIGN act TO <act_standard_table>.
            ASSIGN exp TO <exp_standard_table>.

            DATA(lv_std_table_assert_failed) =
              assert_standard_table(
                EXPORTING iv_clear_table_ind = abap_true
                          iv_table_name     = field_name  "Todo: delete Hungarian naming
                          iv_quit           = quit
                CHANGING  ct_act_table = <act_standard_table>
                          ct_exp_table = <exp_standard_table> ).

            IF lv_std_table_assert_failed = abap_true.
              assertion_failed = abap_true.
            ENDIF.

            REFRESH <act_standard_table>.

        ENDCASE.

      WHEN cl_abap_typedescr=>kind_struct.

        DATA(lr_act_struct_descr) = CAST cl_abap_structdescr( lr_act_type_descr ).

        DATA(lt_components) = lr_act_struct_descr->get_components( ).

        LOOP AT lt_components
          ASSIGNING FIELD-SYMBOL(<ls_component>).

          "Todo: testen met structure includes

          ASSIGN COMPONENT <ls_component>-name
            OF STRUCTURE act
            TO FIELD-SYMBOL(<component_act>).

          ASSIGN COMPONENT <ls_component>-name
            OF STRUCTURE exp
            TO FIELD-SYMBOL(<component_exp>).

          DATA(lv_assert_failed) =
            zcl_abap_unit_assert=>assert_equals_by_ref(
              EXPORTING
                field_name           =
                  COND string( WHEN field_name IS NOT INITIAL THEN field_name && |-| ELSE || ) &&
                  <ls_component>-name
                ignore_hash_sequence = ignore_hash_sequence
                tol                  = tol
                msg                  = msg
                level                = if_aunit_constants=>severity-medium
                quit                 = if_aunit_constants=>quit-test
                skip_default_assert  = abap_true
              CHANGING
                act                  = <component_act>
                exp                  = <component_exp> ).

          IF lv_assert_failed = abap_true.
            assertion_failed = abap_true.
          ENDIF.

        ENDLOOP.

        "Todo: loop

    ENDCASE.


    IF skip_default_assert  = abap_false.

      "Assert the rest in a standard way.
      DATA(lv_sap_assertion_failed) = cl_abap_unit_assert=>assert_equals(
          act                  = act
          exp                  = exp
          ignore_hash_sequence = ignore_hash_sequence
          tol                  = tol
          msg                  = msg
          level                = if_aunit_constants=>severity-medium
          quit                 = if_aunit_constants=>quit-test ).

      IF lv_sap_assertion_failed = abap_true.
        assertion_failed = abap_true.
      ENDIF.

    ENDIF.

  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Protected Method ZCL_ABAP_UNIT_ASSERT=>ASSERT_STANDARD_TABLE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_CLEAR_TABLE_IND             TYPE        ABAP_BOOL
* | [--->] IV_TABLE_NAME                  TYPE        TYPENAME
* | [--->] IV_QUIT                        TYPE        INT1
* | [<-->] CT_ACT_TABLE                   TYPE        STANDARD TABLE
* | [<-->] CT_EXP_TABLE                   TYPE        STANDARD TABLE
* | [<-()] RV_ASSERT_FAILED               TYPE        ABAP_BOOL
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD assert_standard_table.

    "**************************************************************
    "Sort tables
    SORT ct_act_table.
    SORT ct_exp_table.

    "**************************************************************
    "Check line count
    DATA(act_table_line_count) = lines( ct_act_table ).
    DATA(exp_table_line_count) = lines( ct_exp_table ).

    DATA msg TYPE c LENGTH 200.

    DATA(table_line_count_failed) = cl_abap_unit_assert=>assert_equals(
        act                  = act_table_line_count
        exp                  = exp_table_line_count
        msg                  =
          |Table line count | &&
          COND string( WHEN iv_table_name IS NOT INITIAL THEN |of | && iv_table_name ELSE || ) &&
          | are not equal.|
        quit                 = if_aunit_constants=>no ).

    "**************************************************************
    "Set max. field differences
    IF table_line_count_failed = abap_true.
      DATA(field_diff_max_count) = 5.
      rv_assert_failed = abap_true.
    ELSE.
      field_diff_max_count = 10.
    ENDIF.

    "**************************************************************
    "Loop at records
    DATA(field_diff_count) = 0.

    LOOP AT ct_act_table
      ASSIGNING FIELD-SYMBOL(<ls_act_record>).

      DATA(lv_tabix) = sy-tabix.

      READ TABLE ct_exp_table
        INDEX lv_tabix
        ASSIGNING FIELD-SYMBOL(<ls_exp_record>).

      DATA(lr_struct_descr) = CAST cl_abap_structdescr( cl_abap_structdescr=>describe_by_data( <ls_act_record> ) ).

      DATA(lt_components) = lr_struct_descr->get_components( ).

      LOOP AT lt_components
        ASSIGNING FIELD-SYMBOL(<ls_comp>).

        ASSIGN COMPONENT <ls_comp>-name
          OF STRUCTURE <ls_act_record>
          TO FIELD-SYMBOL(<ls_act_value>).

        ASSIGN COMPONENT <ls_comp>-name
          OF STRUCTURE <ls_exp_record>
          TO FIELD-SYMBOL(<ls_exp_value>).

        DATA(lv_assert_failed) =
         cl_abap_unit_assert=>assert_equals(
           act = <ls_act_value>
           exp = <ls_exp_value>
           msg = |Standard table: | && iv_table_name && |, | &&
             |index: | && lv_tabix && |, | &&
             |field: | && <ls_comp>-name
            quit = if_aunit_constants=>no ).

        IF lv_assert_failed = abap_true.

          field_diff_count = field_diff_count + 1.

          IF field_diff_count >= field_diff_max_count.

            cl_abap_unit_assert=>fail(
              msg = |Maximum number of field differences { field_diff_max_count } is reached. There might be more differences.|
              quit = if_aunit_constants=>no ).

            EXIT.

          ENDIF.

          rv_assert_failed = abap_true.

        ENDIF.

      ENDLOOP.

    ENDLOOP.

    IF iv_clear_table_ind = abap_true.
      REFRESH ct_act_table.
      REFRESH ct_exp_table.
    ENDIF.

  ENDMETHOD.
ENDCLASS.

No comments:

Post a Comment