Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 

Write a third unit test


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

Part 8 - Fix the production code bug identified by the second unit test


To recap from the preceding blog, we fixed the production code highlighted by a unit test failure, which caused the unit test to now pass and the production code, when executed, to retrieve the correct rows from the persistence repository. Unfortunately, we also found that when we request the output in ALV grid format we still are presented with the ALV report in classic list format. 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
.
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 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, 2 test methods



Writing our third 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
, set_alv_function_module_name
for testing
.

Then include the following method implementation after the implementation for method get_flights_via_carrier:
  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.

Here we’ve added a third unit test method named set_alv_function_module_name to the unit test class. Notice that this unit test method calls production code subroutine set_alv_function_module_name twice: once with the signature parameter alv_style_grid set to a value of space, then a second time with it set to a value of ‘X’. Notice also that this time we are calling assertion method assert_char_cp (assert character string contains pattern) of class cl_abap_unit_assert twice, once after each of the two interactions with the production subroutine set_alv_function_module_name, and again we have included a quit parameter on both calls to prevent the first assertion failure from causing the unit test method to be discontinued immediately.

Notice in addition that each call to the assertion method includes the parameter level = cl_aunit_assert=>tolerable. This indicates how the severity level should be set for messages appearing in the ABAP Unit: Results Display report. The default level setting for all assertion methods of class cl_abap_unit_assert is “critical”, meaning that an assertion failure will be reported with “error” message severity. Setting it to “tolerable” indicates the preference to have this assertion failure reported with “warning” severity. Warning messages will appear using yellow severity icons in the ABAP Unit: Results Display report whereas error messages appear using red severity icons.

Increasing the percentage of production code covered by automated unit tests


Notice also that each new unit test method added to this unit test class causes yet another production code subroutine to be subjected to unit testing. Simply by including additional unit tests, little by little, eventually having one for every production procedure, we can assure the quality of the production code when all those unit tests pass. Each unit test contributes to what is known as the coverage of the production code by automated unit tests. As the percentage of production code covered by unit tests increases, so too does the level of production code quality, and by extension the more confident the developer can be regarding the robustness of the program.

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 set_alv_function_module_name triggers a warning and that a status message appears indicating:

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


Subroutine set_alv_function_module_name was invoked twice by the unit test method, but we see only a single failure marked as a warning. 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 indicates an expected value ‘*GRID*’ but an actual value of ‘REUSE_ALV_LIST_DISPLAY’. Accordingly, the unit test has revealed yet another bug for us to fix.

Summary


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

  • Unit test assertion class cl_abap_unit_assert includes a method capable of determining whether a string contains a specified pattern: assert_char_cp.

  • All unit test assertion methods of class cl_abap_unit_assert enable overriding the default value of “critical” indicating the severity of messages appearing in the ABAP Unit: Results Display report.

  • Adding additional unit tests, one by one, eventually can result in complete unit test coverage for all the production procedures, raising our confidence in the quality of the production code.


What’s next?


By inspecting subroutine set_alv_function_module_name we should be able to determine why one of its test executions passed and the other failed. This will be covered in the blog representing part 10 of this 10-part series Getting acquainted with automating ABAP unit testing.