Saturday, 2 January 2021

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.

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.

No comments:

Post a Comment