Getting acquainted with automating ABAP unit testing – Part 4
Complete the process of writing the first unit test
This blog represents part 4 of this 10-part series Getting acquainted with automating ABAP unit testing.
Part 3 – Continue the process of writing the first unit test
To recap from the preceding blog, we applied the “for testing” clause to the one and only unit test method defined for the local class named tester, resulting in that empty method being invoked by the test runner of the ABAP Unit Testing Framework. 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 'XFLIGHT' . 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. 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. Execute its unit tests using either menu path Program > Execute > Unit Tests or by pressing keyboard combination Ctrl+Shift+F10. You should find the following status message appears at the bottom of the screen:
Processed: 1 program, 1 test classes, 1 test methods
Specifying testing activities to be performed by a unit test method
Despite executing a single unit test method, that method was devoid of any ABAP code, so let’s change that right now. Change unit test method set_alv_field_catalog so that it contains the following ABAP code:
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. call method cl_abap_unit_assert=>assert_not_initial exporting act = alv_fieldcat_stack msg = 'ALV fieldcatalog is empty' . endmethod.
Afterward, activate the program.
Let’s investigate what this method is doing now. First, it defines a data field named alv_fieldcat_stack. It then contains some comments describing the conditions under which a call to subroutine set_alv_field_catalog should result in no field catalog being created. This is followed by a call to subroutine set_alv_field_catalog to create a field catalog into field alv_fieldcat_stack for the structure named by field flights_table_name. So far everything it is doing conforms with the procedural style of writing ABAP code and should be familiar to you.
The call to the subroutine is followed by our first example of a direct communication by the unit test code with the test runner. Specifically, the call method statement is indicating to call static method assert_not_initial of global class cl_abap_unit_assert, indicating on its “act” (actual value) parameter the name of the variable to be asserted is not initial, and indicating on its “msg” (assertion failure message) parameter a message that should appear in the unit test run failure report, should a failure occur, alerting us that “ALV fieldcatalog is empty”.
Class cl_abap_unit_assert is part of the ABAP Unit Testing Framework and provides a variety of assertion methods that can be invoked by the unit test method to determine the validity of results arising from interacting with fragments of the production code. In this case the interaction by the unit test with a fragment of production code was a call to subroutine set_alv_field_catalog; the content of field alv_fieldcat_stack will determine whether or not it was able to create a corresponding list of ALV field catalog entries for the specified structure. Due to the name of the method and the parameters specified on the call method statement, the assumption being made by the unit test is that field alv_fieldcat_stack should not be empty after the call to the subroutine. When field alv_fieldcat_stack is not empty, then the unit test has passed, but when it is empty then the unit test has failed. This outcome is determined when field alv_fieldcat_stack, specified on the “act” parameter, is checked by a method whose name is “assert_not_initial”.
This illustrates an example of what are known as self-checking tests. The unit test will run to completion and issue a simple status message of completion when all unit tests have passed; otherwise, when unit test failures have occurred, the ABAP Unit: Results Display report will be presented and offer an analysis of the failures.
Running the unit test again
Execute the unit tests again. This time we should still see the following status message appearing at the bottom of the screen
Processed: 1 program, 1 test classes, 1 test methods
but this time the screen presented is the ABAP Unit: Results Display report indicating that test method set_alv_field_catalog triggered a failure. The reason for the failure is presented in the analysis section appearing in the lower right block of the ABAP Unit: Results Display report, which shows that a non-initial value was expected, but field alv_fieldcat_stack is empty (it is initial, contrary to the name of the assertion method called). The reason it is empty is that field flights_table_name contains the value ‘XFLIGHT’ instead of the intended value ‘SFLIGHT’. Accordingly, the call to production subroutine set_alv_field_catalog was unable to produce a set of valid ALV field catalog entries for a structure that does not exist.
Executing the program again
Despite the changes to the code, we should continue to find no difference when we execute the program and provide Airline ‘AA’ – it still results in an error message indicating “No flights match carrier AA”. Try this now.
Speculating on the origin of a myth
We now see that our example program contains a single local unit test class, a necessity when writing automated unit tests for ABAP. That local class contains a method having a call to static method assert_not_initial of global class cl_abap_unit_assert. So for this single unit test we see both a local class and a global class involved in facilitating the test. Accordingly, the program now contains object-oriented code we wrote ourselves and calls other object-oriented code provided by the Automated Unit Test Framework assertion class.
We already know that to include any automated unit tests for an ABAP program, even for one that has no object-oriented statements in its production code, will require at least one local object-oriented class to be defined in the program. This is accurately described by the following sentence:
ABAP programs containing automated unit tests will require the unit testing code to be defined using the object-oriented model.
This sentence easily can become shorted to the following, with the important phrase “unit testing” dropped in the process:
ABAP programs containing automated unit tests will require the code to be defined using the object-oriented model.
It then becomes easy to rephrase the sentence to state the following:
Automated unit tests can be applied only to programs written using the object-oriented model.
This might explain why so many ABAP programmers believe that only programs written using the object-oriented model can contain automated unit tests. But as just demonstrated, an automated unit test is quite capable of testing procedural subroutines defined using the form-endform construct. The only object-oriented components of this program are contained solely within the self-checking test. The remainder of the program is purely procedural.
So, let’s summarize what we have learned here:
- The activities specified for a unit test method will include an interaction with a fragment of the production code (in this case it was a call to subroutine set_alv_field_catalog).
- The activities specified for a unit test method also will include a call to a static assertion method of class cl_abap_unit_assert to determine whether the interaction with the production code resulted in a passing test or a failing test.
- Tests that require no user involvement by the person executing the unit tests are known as self-checking tests, and, as we see with this example, this unit test required no user involvement to run to completion (other than, of course, to initiate running all the automated unit tests).
- The reason so many ABAP programmers believe that only programs written using the object-oriented model can contain automated unit tests may be due to having taken the requirement to write its unit tests using object-oriented code and extending that to apply to the entire program.
We’ve uncovered the first of many problems with this production code – no wonder it has been issuing error messages when executed. Now the code needs to be changed so that this unit test passes. This will be covered in the blog representing part 5 of this 10-part series Getting acquainted with automating ABAP unit testing.