Write a second unit test
This blog represents part 7 of this 10-part series Getting acquainted with automating ABAP unit testing.
Part 6 – Using the correct method call syntax to request a unit test assertion
To recap from the preceding blog, we had changed the call to method assert_not_initial of class cl_abap_unit_assert so that it uses the standalone static method call syntax instead of the former dynamic method call syntax. We also know there is a bug in the production code: the ALV report presents rows for the wrong airline. 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 . 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. 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. It should result in the following message presented in the ABAP editor:
Processed: 1 program, 1 test classes, 1 test methods
Writing our second unit test method
Change the methods statement in the definition portion of class “tester” so that it looks like this:
methods : set_alv_field_catalog for testing , get_flights_via_carrier for testing .
Then include the following method implementation after the implementation for method set_alv_field_catalog:
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.
Here we’ve added a second unit test method named get_flights_via_carrier to the unit test class. Notice that this unit test method calls production code subroutine get_flights_via_carrier once for each of three different carriers: ‘AA’, ‘LH’ and ‘UA’. Notice also that the call to the assertion method assert_equals of class cl_abap_unit_assert includes a quit parameter, one that indicates not to quit. For all the assertion methods of class cl_abap_unit_assert, the default setting for the quit parameter is “method”, meaning the unit test “method” is discontinued upon encountering the first assertion failure unless it is overridden. When the quit parameter is overridden to indicate no quitting, then the unit test method will continue executing even after an assertion failure has been detected.
Running the unit tests again
Activate the program, then execute its unit tests. You should find that the ABAP Unit: Results Display report appears indicating test method get_flights_via_carrier has triggered two failure messages, and that a status message appears indicating:
Processed: 1 program, 1 test classes, 2 test methods
Subroutine get_flights_via_carrier was invoked 3 times by the unit test method, but we see only two failures: Once when invoked with airline code ‘AA’ and once again when invoked with airline code ‘UA’. This is a clue that one of the test executions of this subroutine did not trigger an assertion failure. The analysis section of the ABAP Unit: Results Display report shows that each failure encountered an actual value of ‘LH’ instead of the expected airline code. Accordingly, the unit test has revealed yet another bug for us to fix.
So, let’s summarize what we have learned here:
- A single unit test class may have multiple unit test methods.
- A unit test method may make multiple interactions with fragments of the production code.
- By default, a unit test method typically will discontinue its execution upon encountering the first assertion failure.
- The default behavior of a unit test method to discontinue its execution upon encountering the first assertion failure can be overridden by including a quit parameter indicating a value allowing the unit test to continue executing.
By inspecting subroutine get_flights_via_carrier we should be able to determine why one of its test executions passed and the two others failed. This will be covered in the blog representing part 8 of this 10-part series Getting acquainted with automating ABAP unit testing.