Tuesday 27 April 2021

Getting acquainted with automating ABAP unit testing – Part 8

Part 7 – Write a second unit test

To recap from the preceding blog, we had added a new unit test method to call subroutine get_flights_via_carrier three times, each time using a different carrier value, and found that this unit test method issued two failures, suggesting that one of the carrier values did not cause a test failure. Here is the source code as we left it in the previous blog:

program.

*----------------------------------------------------------------------

* Define Selection Texts as follows:

*   Name     Text

*   -------- -------------------------------

*   CARRIER  Airline

*   DISCOUNT Airfare discount percentage

*   VIA_GRID Display using alv grid

*   VIA_LIST Display using alv classic list

*

*==========================================

*

*   G l o b a l   F i e l d s

*

*========================================

types            : flights_row    type sflight

                 , flights_list   type standard table

                                    of flights_row

                 , carrier        type s_carr_id

                 , discount       type s_discount

                 .

constants        : flights_table_name

                                  type tabname   value 'SFLIGHT'

                 .

data             : flights_count  type int4

                 , flights_stack  type flights_list

                 .

*=========================================

*

*   S c r e e n   C o m p o n e n t s

*

*=========================================

selection-screen : begin of block selcrit with frame title tselcrit.

parameters       :   carrier      type carrier obligatory

                 ,   discount     type discount

                 ,   via_list     radiobutton group alv

                 ,   via_grid     radiobutton group alv

                 .

selection-screen : end   of block selcrit.

*==========================================

*

*   C l a s s i c   P r o c e d u r a l   E v e n t s

*

*=========================================

initialization.

    tselcrit                      = 'Selection criteria' ##NO_TEXT.

at selection-screen.

    if sy-ucomm ne 'ONLI'.

      return.

    endif.

    " Diagnose when user has specified an invalid discount:

    if discount gt 100.

      message w000(0k) with 'Fare discount percentage exceeding 100' ##NO_TEXT

                            'will be ignored'                        ##NO_TEXT

                            space

                            space

                            .

    endif.

    " Get list of flights corresponding to specified carrier:

    perform get_flights_via_carrier using carrier.

    " Diagnose when no flights for this carrier:

    if flights_count le 00.

      message e000(0k) with 'No flights match carrier' ##NO_TEXT

                            carrier

                            space

                            space

                            .

    endif.

start-of-selection.

end-of-selection.

    perform present_report using discount

                                 via_grid.

*=======================================

*

*   S u b r o u t i n e s

*

*======================================

form get_flights_via_carrier using carrier

                                     type carrier.

    clear flights_stack.

    if carrier is not initial.

      try.

        select *

          into table flights_stack

          from (flights_table_name)

         where carrid               eq 'LH'

             .

      catch cx_root ##NO_HANDLER ##CATCH_ALL.

        " Nothing to do other than intercept potential exception due to

        " invalid dynamic table name

      endtry.

    endif.

    describe table flights_stack lines flights_count.

endform.

form present_report using discount

                            type discount

                          via_grid

                            type xflag.

    perform show_flights_count.

    perform show_flights using discount

                               via_grid.

endform.

form show_flights_count.

    " Show a message to accompany the alv report which indicates the

    " number of flights for the specified carrier:

    message s000(0k) with flights_count

                          'flights are available for carrier' ##NO_TEXT

                          carrier

                          space

                          .

endform.

form show_flights using flight_discount

                          type num03

                        alv_style_grid

                          type xflag.

    data         : alv_layout     type slis_layout_alv

                 , alv_fieldcat_stack

                                  type slis_t_fieldcat_alv

                 , alv_display_function_module

                                  type progname

                 .

    " Adjust flights fare by specified discount:

    perform apply_flight_discount using flight_discount.

    " Get total revenue for flight as currently booked:

    perform adjust_flight_revenue.

    " Set field catalog for presenting flights via ALV report:

    perform set_alv_field_catalog using flights_table_name

                               changing alv_fieldcat_stack.

    if alv_fieldcat_stack is initial.

      message e000(0k) with 'Unable to resolve field catalog for ALV report' ##NO_TEXT

                            space

                            space

                            space

                            .

    endif.

    " Set name of alv presentation function module based on user selection:

    perform set_alv_function_module_name using alv_style_grid

                                      changing alv_display_function_module.

    " Present flights via ALV report:

    call function alv_display_function_module

      exporting

        is_layout                 = alv_layout

        it_fieldcat               = alv_fieldcat_stack

      tables

        t_outtab                  = flights_stack

      exceptions

        others                    = 09

        .

    if sy-subrc ne 00.

      message e000(0k) with 'Unable to present ALV report' ##NO_TEXT

                            space

                            space

                            space

                            .

    endif.

endform.

form apply_flight_discount using flight_discount

                                   type discount.

    constants    : percent_100    type int4

                                                 value 110

                 .

    field-symbols: <flights_entry>

                                  type flights_row

                 .

    if flight_discount le 00.

      return.

    endif.

    if flight_discount gt percent_100.

      return.

    endif.

    " Apply the specified discount against all flights:

    loop at flights_stack assigning

           <flights_entry>.

      perform calculate_discounted_airfare using <flights_entry>-price

                                                 flight_discount

                                        changing <flights_entry>-price

                                                 sy-subrc

                                                 .

    endloop.

endform.

form adjust_flight_revenue.

    field-symbols: <flights_entry>

                                  type flights_row

                 .

    " Calculate flight revenue based on airfare and number of occupied seats:

    loop at flights_stack assigning

           <flights_entry>.

      perform get_flight_revenue using <flights_entry>-price

                                       <flights_entry>-seatsocc

                              changing <flights_entry>-paymentsum

                                       .

    endloop.

endform.

form get_flight_revenue using fare_price

                                type s_price

                              number_of_passengers

                                type s_seatsocc

                     changing flight_revenue

                                type s_sum

                              .

    flight_revenue                = fare_price * number_of_passengers.

endform.

form calculate_discounted_airfare using full_fare

                                          type s_price

                                        discount

                                          type s_discount

                               changing discount_fare

                                          type s_price

                                        return_code

                                          type sysubrc

                                        .

    constants    : highest_discount_percentage

                                  type int4      value 110

                 .

    data         : discount_multiplier

                                  type p decimals 3

                 .

    return_code                   = 00.

    if discount gt highest_discount_percentage.

      return_code                 = 01.

      return.

    endif.

    discount_multiplier           = ( 100 - discount ) / 100.

    discount_fare                 = full_fare * discount_multiplier.

endform.

form set_alv_field_catalog using structure_name

                                   type tabname

                        changing alv_fieldcat_stack

                                   type slis_t_fieldcat_alv.

    " Set field catalog for presenting ALV report:

    call function 'REUSE_ALV_FIELDCATALOG_MERGE'

      exporting

        i_structure_name          = structure_name

      changing

        ct_fieldcat               = alv_fieldcat_stack

      exceptions

        others                    = 0

        .

endform.

form set_alv_function_module_name using alv_style_grid

                                          type xflag

                               changing alv_display_function_module

                                          type progname.

    constants    : alv_list_function_module

                                  type progname  value 'REUSE_ALV_LIST_DISPLAY'

                 , alv_grid_function_module

                                  type progname  value 'REUSE_ALV_LIST_DISPLAY'

                 .

    " Set name of function module corresponding to selected style of alv

    " report - list or grid:

    if alv_style_grid is initial.

      alv_display_function_module = alv_list_function_module.

    else.

      alv_display_function_module = alv_grid_function_module.

    endif.

endform.

*=========================================

*

*   A B A P   U n i t   T e s t   c o m p o n e n t s

*

*=========================================

class tester                           definition

                                       final

                                       for testing

                                       risk level harmless

                                       duration short

                                       .

  private section.

    methods      : set_alv_field_catalog

                     for testing

                 , get_flights_via_carrier

                     for testing

                 .

endclass.

class tester                           implementation.

  method set_alv_field_catalog.

    data         : alv_fieldcat_stack

                                  type slis_t_fieldcat_alv

                 .

    " Setting the alv field catalog in the executable program uses a

    " parameter to specify the name of the structure to be used.  If

    " this name is invalid, no field catalog entries will result.  Here

    " we insure that the string which specifies the name of the structure

    " contains a valid structure name.

    perform set_alv_field_catalog using flights_table_name

                               changing alv_fieldcat_stack.

    cl_abap_unit_assert=>assert_not_initial(

      act                         = alv_fieldcat_stack

      msg                         = 'ALV fieldcatalog is empty'

      ).

  endmethod.

  method get_flights_via_carrier.

    constants    : lufthansa      type s_carr_id value 'LH'

                 , united_airlines

                                  type s_carr_id value 'UA'

                 , american_airlines

                                  type s_carr_id value 'AA'

                 .

    data         : failure_message

                                  type string

                 , flights_entry  like line

                                    of flights_stack

                 , carrier_id_stack

                                  type table

                                    of s_carr_id

                 , carrier_id_entry

                                  like line

                                    of carrier_id_stack

                 .

    " This unit test is modelled after the example unit test presented

    " in the book "ABAP Objects - ABAP Programming in SAP NetWeaver",

    " 2nd edition, by Horst Keller and Sascha Kruger (Galileo Press,

    " 2007, ISBN 978-1-59229-079-6).  Refer to the sample listing 13.3

    " starting on page 964.  Here we insure that the list of flights

    " retrieved contains only those flights for the specified carrier.

    append: lufthansa             to carrier_id_stack

          , united_airlines       to carrier_id_stack

          , american_airlines     to carrier_id_stack

          .

    loop at carrier_id_stack

       into carrier_id_entry.

      concatenate 'Selection of'

                  carrier_id_entry

                  'gives different airlines'

             into failure_message separated by space.

      perform get_flights_via_carrier using carrier_id_entry.

      " We have specified a quit parameter for the next assertion.

      " The default action is to terminate the test method upon encountering

      " an error.  We do not want to terminate this test method with the

      " first error because we intend to run this test for multiple carriers

      " as identified in the outer loop, allowing ABAP Unit test errors to

      " be issued for whichever carriers they apply.

      " Notice also that the vale specified for the quit parameter is a

      " constant defined in class cl_aunit_assert.  Class cl_aunit_assert

      " is the name of the first generation of ABAP Unit assertion class.

      " It still exists and still can be used, but SAP has since superseded

      " this class with the more descriptively named assertion class

      " cl_abap_unit_assert.  We are using the old class name here because its

      " static attributes were not made available to class cl_abap_unit_assert.

      loop at flights_stack

         into flights_entry.

        cl_abap_unit_assert=>assert_equals(

          act                     = flights_entry-carrid

          exp                     = carrier_id_entry

          msg                     = failure_message

          quit                    = cl_aunit_assert=>no

          ).

        if flights_entry-carrid ne carrier_id_entry.

          exit. " loop at flights_stack

        endif.

      endloop.

    endloop.

  endmethod.

endclass.

Running existing unit tests

Fire up the ABAP Editor and retrieve or create the program containing the code shown above, then activate it and execute its unit test. The ABAP Unit: Results Display report appears accompanied by the following status message appearing at the bottom of the screen:

Processed: 1 program, 1 test classes, 2 test methods

The report indicates two messages in the Failures and Messages section appearing in the upper right block. The analysis section shows the reason for the failure: a difference between the expected value and the actual value on the call to the assertion method. Information appearing in the analysis section is related to the message selected in the Failures and Messages section. Accordingly, clicking each failure message will provide an analysis specific to that failure.

Identifying and fixing the reason for the unit test failure

Locate subroutine get_flights_via_carrier. Notice it has a signature through which it accepts a carrier value. Stepping through the code it soon becomes clear why one unit test call to this subroutine passed and the other two failed. It is because the carrier value accepted through the subroutine signature is ignored. Instead, the “select” statement has a hard-coded value of carrier ‘LH’ specified in its “where” clause. Accordingly, we should expect to have rows retrieved from table SFLIGHT only when the signature parameter value matches this hard-coded value and should expect there to be no rows retrieved when it does not match. This explains why only one of the three unit test calls to this subroutine did not result in a failure.

The fix is simple: Change the “where” clause of the “select” statement, replacing the hard-coded value ‘LH’ with the signature parameter named carrier.

Running the unit test again

After applying the change, activate the program, then execute its unit tests. You should find that a status message appears indicating:

Processed: 1 program, 1 test classes, 2 test methods

and the ABAP Unit: Results Display report is not displayed, meaning there no longer are any unit test failures.

Executing the program again

Again execute the program and provide Airline ‘AA’. Now the program produces a classic ALV report containing flights for the specified carrier. Execute it again, this time selecting the radio button for the ALV grid report. Again we are presented with the classic ALV report, yet another bug to be fixed.

No comments:

Post a Comment