Skip to Content
Technical Articles

Using a super test class in unit tests

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 Michael Keller.  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
You must be Logged on to comment or reply to a post.
  • Another fine thought to think about 🙂 The idea with the abstract classes is very interesting. I have to take a closer look at that when I get the chance.