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: 
jrgkraus
Active Contributor
1,598
Writing abap unit tests is - just like writing production code - a matter of style. We should apply all the principles for clean coding also to our unit tests. Remember, an error can occur as well in a unit test as in your production code. Remember furthermore, your unit test coding may be read by others and you want them to understand it easily.

In order to avoid duplicate coding and enhance readability of tests, I use the approach of using abstract test classes.

To illustrate the idea, i refer to this very nice blog by keller.m.  For my example, I use the classes that are proposed there (ZCL_FUEL, ZCL_OXYGENE, ZCL_HEAT, ZCL_FIRE) and refactor the test class.

The original test class uses one class and four methods. For my example, I will only refactor the "SI_" test methods - which illustrate the setter injection.
CLASS ltc_fire DEFINITION FOR TESTING DURATION LONG RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS ci_fire_when_everything_fits FOR TESTING.
METHODS ci_no_fire_not_enough_heat FOR TESTING.
METHODS si_fire_when_everything_fits FOR TESTING.
METHODS si_no_fire_not_enough_heat FOR TESTING.
ENDCLASS.

CLASS ltc_fire IMPLEMENTATION.
METHOD ci_fire_when_everything_fits.
DATA(lo_fuel) = NEW zcl_fuel( iv_flammable = abap_true ).
DATA(lo_oxygen) = NEW zcl_oxygen( iv_sufficient_quantity = abap_true ).
DATA(lo_heat) = NEW zcl_heat( iv_sufficient_heat = abap_true ).

DATA(lo_fire) = NEW zcl_fire( io_fuel = lo_fuel
io_oxygen = lo_oxygen
io_heat = lo_heat ).

DATA(lv_is_burning) = lo_fire->is_burning( ).

cl_abap_unit_assert=>assert_true( act = lv_is_burning ).
ENDMETHOD.

METHOD ci_no_fire_not_enough_heat.
DATA(lo_fuel) = NEW zcl_fuel( iv_flammable = abap_true ).
DATA(lo_oxygen) = NEW zcl_oxygen( iv_sufficient_quantity = abap_true ).
DATA(lo_heat) = NEW zcl_heat( iv_sufficient_heat = abap_false ).

DATA(lo_fire) = NEW zcl_fire( io_fuel = lo_fuel
io_oxygen = lo_oxygen
io_heat = lo_heat ).

DATA(lv_is_burning) = lo_fire->is_burning( ).

cl_abap_unit_assert=>assert_false( act = lv_is_burning ).
ENDMETHOD.

METHOD si_fire_when_everything_fits.
DATA(lo_fuel) = NEW zcl_fuel( iv_flammable = abap_true ).
DATA(lo_oxygen) = NEW zcl_oxygen( iv_sufficient_quantity = abap_true ).
DATA(lo_heat) = NEW zcl_heat( iv_sufficient_heat = abap_true ).

DATA(lo_fire) = NEW zcl_fire( ).
lo_fire->set_fuel( lo_fuel ).
lo_fire->set_head( lo_heat ).
lo_fire->set_oxygen( lo_oxygen ).

DATA(lv_is_burning) = lo_fire->is_burning( ).

cl_abap_unit_assert=>assert_true( act = lv_is_burning ).
ENDMETHOD.

METHOD si_no_fire_not_enough_heat.
DATA(lo_fuel) = NEW zcl_fuel( iv_flammable = abap_true ).
DATA(lo_oxygen) = NEW zcl_oxygen( iv_sufficient_quantity = abap_true ).
DATA(lo_heat) = NEW zcl_heat( iv_sufficient_heat = abap_false ).

DATA(lo_fire) = NEW zcl_fire( ).
lo_fire->set_fuel( lo_fuel ).
lo_fire->set_head( lo_heat ).
lo_fire->set_oxygen( lo_oxygen ).

DATA(lv_is_burning) = lo_fire->is_burning( ).

cl_abap_unit_assert=>assert_false( act = lv_is_burning ).
ENDMETHOD.
ENDCLASS.

In this unit test, several test cases appear as test methods. The idea is, to make a test class of each test case. Leave the necessary preparation of test data (and test doubles, if needed) to the abstract test class and pick the needed values from there. In this case, the unit test class would transform to:
class test_si definition for testing duration long risk level harmless
abstract.
protected section.
data cut type ref to zcl_fire.

"prerequisite flags set by the test case
data given_flammable type abap_bool.
data given_sufficient_oxygen type abap_bool.
data given_enough_heat type abap_bool.

"expected result of the test call
data expected_result type abap_bool.

"general setup and test
methods setup_low.
methods test_low.
endclass.

class test_si implementation.
method setup_low.
cut = new #( ).
cut->set_fuel( new zcl_fuel( given_flammable ) ).
cut->set_heat( new zcl_heat( given_enough_heat ) ).
cut->set_oxygen( new zcl_oxygen( given_sufficient_oxygen ) ).
endmethod.

method test_low.
cl_abap_unit_assert=>assert_equals(
exp = expected_result
act = cut->is_burning( ) ).
endmethod.
endclass.

class test_everything_fits_si
definition for testing duration long risk level harmless
inheriting from test_si
final.
private section.
methods setup.
methods test for testing.
endclass.

class test_everything_fits_si implementation.
method setup.
"set concrete prerequisites
given_flammable = abap_true.
given_enough_heat = abap_true.
given_sufficient_oxygen = abap_true.
setup_low( ).
endmethod.

method test.
"set concrete expected result
expected_result = abap_true.
test_low( ).
endmethod.

endclass.

class test_not_enough_heat_si
definition for testing duration long risk level harmless
inheriting from test_si
final.
private section.
methods setup.
methods test for testing.
endclass.

class test_not_enough_heat_si implementation.
method setup.
given_flammable = abap_true.
given_enough_heat = abap_false.
given_sufficient_oxygen = abap_true.
setup_low( ).
endmethod.

method test.
expected_result = abap_false.
test_low( ).
endmethod.

endclass.

In the abstract class, we have:

  • setup_low where the class under test is instantiated with parameters that come from attributes that can be set by the concrete test case

  • test_low where an abap unit call is done in order to test our method is_burning. It also uses parameters from the class attributes.


Each test case (everything_fits and not_enough_heat) has its own test class. We only set our parameters (given_... for the prerequisites, expected_... for the results to compare) and call the methods from the abstract class.

If we need a new test case, for example not_enough_oxygen, we just copy the last final test class and change two parameters:
class test_not_enough_oxygene    " name changed after copy
definition for testing duration long risk level harmless
inheriting from test
final.
private section.
methods setup.
methods test for testing.
endclass.

class test_not_enough_oxygene implementation. " name changed
method setup.
given_flammable = abap_true.
given_enough_heat = abap_true. " changed from abap_false
given_sufficient_oxygen = abap_false. "changed from abap_true
setup_low( ).
endmethod.

method test.
expected_result = abap_false. " not changed, the result is the same
test_low( ).
endmethod.
1 Comment
Labels in this area