Skip to Content
Technical Articles

Getting acquainted with automating ABAP unit testing – Part 10

Fix the production code bug identified by the third unit test

This blog represents part 10 of this 10-part series Getting acquainted with automating ABAP unit testing.

Part 9 – Write a third unit test

To recap from the preceding blog, we added a new unit test for testing production subroutine set_alv_function_module_name and, when executed, this unit test triggers a warning. 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 carrier
             .
      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
                 , set_alv_function_module_name
                     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.
  method set_alv_function_module_name.
    constants    : list_flag      type xflag     value space
                 , grid_flag      type xflag     value 'X'
                 .
    data         : alv_display_function_module
                                  type progname
                 .
    " The user may select to display the report using alv classic list
    " or alv grid control.  The function modules facilitating these use
    " the same parameter interface and the name of each one contains the
    " string "LIST" or "GRID" respectively.  Here we insure that we
    " get the correct function module name resolved when we provide the
    " flag indicating whether or not to use the grid control.
    perform set_alv_function_module_name using list_flag
                                      changing alv_display_function_module.
    " Here we use the level parameter to indicate that although we may
    " get the incorrect name of the function module based on the selection
    " flag, it is not a critial error (the default for not specifying level).
    cl_abap_unit_assert=>assert_char_cp(
          act                     = alv_display_function_module
          exp                     = '*LIST*'
          msg                     = 'Incorrect ALV program name selected'
          level                   = cl_aunit_assert=>tolerable
          quit                    = cl_aunit_assert=>no
          ).
    perform set_alv_function_module_name using grid_flag
                                      changing alv_display_function_module.
    cl_abap_unit_assert=>assert_char_cp(
          act                     = alv_display_function_module
          exp                     = '*GRID*'
          msg                     = 'Incorrect ALV program name selected'
          level                   = cl_aunit_assert=>tolerable
          quit                    = cl_aunit_assert=>no
          ).
  endmethod.
endclass.

Running existing unit tests

Fire up the ABAP Editor and retrieve or create the program containing the program code shown above, activate it and execute its unit tests. 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, 3 test methods

The report indicates in the analysis section the reason for the failure: a character string does not match the expected pattern.

Identifying and fixing the reason for the unit test failure

Locate subroutine set_alv_function_module_name. Notice it has a signature through which it accepts a value indicating whether or not the ALV report should be presented using the grid option, setting the name of the corresponding function module accordingly. We don’t have to look very far when we notice the two constants: one named alv_list_function_module and another named alv_grid_function_module. The problem is that both of these constants have the same value for their respective function module names. This explains why only one of the two unit test calls to this subroutine resulted in a failure.

The fix is simple: Change the value for constant alv_grid_function_module from REUSE_ALV_LIST_DISPLAY to REUSE_ALV_GRID_DISPLAY.

Running the unit tests 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, 3 test methods

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

Raising our level of confidence in changing production code

At this point we should also recognize that not only are there now 3 unit test methods defined for this program, but none of them triggers a failure when all unit tests are executed. It means that the change we just made to the production code – to change the value of a constant defined locally to subroutine set_alv_function_module_name – caused none of the other two previously passing unit tests to now fail. This is profound. It means that when there are unit tests covering other parts of the code, we can apply changes anywhere within the production code where changes would be required, and still have the confidence that the new production changes cause none of the formerly passing unit tests now to fail. This becomes particularly important when the required production changes are more invasive than the simple change we made with this example.

Executing the program again

Again execute the program and provide Airline ‘AA’ and select the ALV classic list option. 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. This time we are presented with the correct grid style ALV report.

Summary

So, let’s summarize what we have learned here:

  • It is easy to cause bugs during development by cutting and pasting constants, then renaming one of them but forgetting to change its value accordingly, which probably explains how this bug was introduced into the production code.
  • Yet again, a minor change applied to the production code can cause a previously failing test now to become a passing test.
  • Applying the fix to resolve the automated unit test failure also resulted in the correct style of ALV report presenting the rows for the specified carrier.
  • When there are multiple unit tests covering the production code and none of them fail after having changed the production code, it raises our confidence that those new production changes have not introduced any new bugs.

What’s next?

This is the final blog in this 10-part series Getting acquainted with automating ABAP unit testing. By now you should have a better understanding of what is involved with writing automated unit tests for ABAP programs. This example program we have been using remains strewn with many other bugs just waiting to cause problems, and we have only scratched the surface of what is possible through automated unit testing of ABAP code.

Are you interested in continuing the process of writing automated unit tests for this program to find the other hidden bugs lying within? Do you feel an overwhelming urge to become more acquainted with automated unit testing? Have you begun to realize how fast and easy it is to write automated unit tests for any ABAP program? Can you already appreciate how such tests can detect when new bugs creep into a program during its maintenance? Are you curious to learn which ABAP statements interfere with automated unit testing and how to accommodate them? Do you want to explore how to use Test-Driven Development to implement new changes to code? Do you find the idea of “designing code for testability” appealing? Do you want to know more about isolating the code to be tested and how to use test doubles, including configurable test doubles, to facilitate it? Would you like to become more familiar with some of the pitfalls to be avoided when writing unit tests? Do you want to learn more about these and other associated concepts?

If you answered “Yes” to any of those questions, then download the .zip file available hereThe downloaded zip file contains document “Automated Unit Testing with ABAP exercises workbook.pdf” in addition to another zip file “exercise programs source code.zip”.

The first 10 programs of the downloaded exercise programs source code are the same as those used throughout this 10-part blog series. The exercises workbook explains what to do and how to proceed for all the remaining exercises. There are so many exercises, each one building on the one preceding it, that if you were to start tomorrow and do a single exercise each day, including weekends and holidays, you will be able to learn something new about ABAP unit testing every day for the next six months. That’s a lot of exercising, but just think how much your unit testing skills will improve and imagine the beneficial effect the experience will have on your approach to unit testing!

3 Comments
You must be Logged on to comment or reply to a post.
  • Hi James,

    thanks a lot for this series of posts and the additional zipped material which I'll try to work through as time allows.

    Is it a fair summary to state that "don't let the perfect be the enemy of the good" applies to unit testing non-OO code? Meaning to just not fret about isolating code or working with test doubles (just yet) and simply create unit tests for what can easily be tested?

    Cheers

    Bärbel

    • Bärbel,

      Yes, that seems to be a fair position to take at this point in the journey toward applying test automation to non-OO code.  Over time as more and more automated unit tests are provided to the code it increases your confidence that subsequent maintenance changes do not cause any previously successful unit tests to fail, or if they do fail, then you'll be able to determine what new change was introduced to cause the failure.  Even very simple automated tests will have a beneficial effect on the quality of the code.

      Regards,

      Jim

      • It's very much a case of "leave code in better condition than when you found it". An improvement is an improvement, even if it doesn't make things perfect.