Friday 13 November 2020

Separate data classes in unit tests

I proposed using an abstract class for the parts of a unit test that do not differ among the various test cases. I also suggested creating a separate class for each test case.

Based on this approach, I now want to describe a technique for separating the test data creation from the test data usage in test doubles.

Note that this approach is especially useful for classes where you put to the test a public method and cover the other methods by the calls of the public one. Classes that respect the single responsibility principle often are made like this.

The template

First, I want to list an eclipse template I use. You can simply transform it into a SE80 template  transforming the ${…} variables into %…% ones.

* ----- super classes for general definitions ----

class test_data definition for testing abstract.

  public section.

    "constants: constant1....

    "data given_...

    "data expected...

    methods constructor.

endclass.

class test_data implementation.

  method constructor.

" set up general test data

  endmethod.

endclass.

class test  definition for testing risk level harmless duration short

            abstract.

  protected section.

    "data iut type ref to ...

    data testdata type ref to test_data.

    " mock up test doubles

"data mocked_...

    "methods mockup_...

    methods setup_low.

    methods test_low.

  private section.

endclass.

class test implementation.

  method setup_low.

    "mocked_... = mockup_...

    "iut = ...

  endmethod.

  method test_low.

    "cl_abap_unit_assert=>assert_equals(

    "  exp = testdata->expected_...

    "  act = iut->....

    "cl_abap_testdouble=>verify_expectations( mocked_get_objects ).

  endmethod.

endclass.

* ---- Test case "${test_case}" -----

class test_data_${test_case} definition for testing inheriting from test_data.

  public section.

    methods constructor.

endclass.

class test_data_${test_case} implementation.

  method constructor.

    super->constructor( ).

    " create specific test data

  endmethod.

endclass.

class test_${test_case} definition for testing risk level harmless duration short

                inheriting from test

                final.

  private section.

    methods setup.

    methods test for testing.

endclass.

class test_${test_case} implementation.

  method setup.

    testdata = new test_data_${test_case}( ).

    setup_low( ).

  endmethod.

  method test.

    test_low( ).

  endmethod.

endclass.

As you may notice, I use IUT (meaning interface unter test) instead of the common CUT (class under test). Normally, my global classes always have a factory in a factory class where backdoor injection is possible. Therefore, I use the interface rather than the class to put the public methods to the test.

The template defines four classes

◉ class TEST_DATA: abstract class where we define constants and general test data used among all test cases

◉ class TEST: abstract class that provides a general setup and test method (SETUP_LOW, TEST_LOW)

◉ TEST_DATA_${test_case}: This is a concrete test case (you enter the concrete name in the template variable). After calling the super->constructor, all general data is set up. After that, we can add more test data to be used.

◉ TEST_${test_case}: this is the real test class, where we find a method for testing and one for the setup. They both call the generic variant from the super class, so normally, there won’t be a need to change the coding that the template suggests

Example coding

As an example, I take a real life class of my daily work where I used the template. It is for a class that reads all employees and their positions belonging to a manager from the HR master data.

The TEST_DATA class looks like this:

class test_data definition for testing.

  public section.

    constants: emp_nr_1         type pernr_d value '1',

               emp_nr_2         type pernr_d value '2',

               (...) many further constants...

               planning_variant type plvar value '01'.

    data given_positions type zif_ca03_hr_get_objects=>positions.

    data given_employees type zcl_hr05_salhist_types=>employees.

    data given_selections type /iwbep/t_mgw_select_option.

    data computed_key_objects type zif_ca03_hr_get_objects=>key_objects.

    data expected_employees type zif_hr05_position_reader=>employees.

    methods constructor.

endclass.

class test_data implementation.

  method constructor.

  endmethod.

endclass.

This generic data class is useful mainly for its constants. Furthermore I declare all the test data objects I need in my test case.

In the first test case “unfiltered”, the test data for this case is being created in the constructor:

class test_data_unfiltered definition for testing inheriting from test_data.

  public section.

    methods constructor.

endclass.

class test_data_unfiltered implementation.

  method constructor.

    super->constructor( ).

    given_positions =

      value #(

        plvar = planning_variant

        otype = 'S'

        ( short = emp_nr_1 objid = pos_nr_1 stext = pos_name_1 )

        ( short = emp_nr_2 objid = pos_nr_2 stext = pos_name_2 )

        ( short = emp_nr_3 objid = pos_nr_3 stext = pos_name_3 )

        ( short = emp_nr_4 objid = pos_nr_4 stext = pos_name_4 ) ).

    " and so on....

  endmethod.

endclass.

in the second test case “filtered”, the test data is set up differently:

class test_data_filtered implementation.

  method constructor.

    super->constructor( ).

    given_selections =

      value #(

        ( property = 'OrgUnit'

          select_options = value #( ( sign = 'I' option = 'EQ'  low = '21' ) ) ) ).

    given_positions =

      value #(

        plvar = planning_variant

        otype = 'S'

        ( short = emp_nr_1 objid = pos_nr_1 stext = pos_name_1 )

        ( short = emp_nr_2 objid = pos_nr_2 stext = pos_name_2 ) ).

    " and so on...

  endmethod.

endclass

In the abstract class TEST, all the test doubles are being created using the data from the (abstract) test class. Note that the TESTDATA object is not being created here, this happens in the concrete test classes.

class test  definition for testing risk level harmless duration short

            abstract.

  protected section.

    data iut type ref to zif_hr05_position_reader.

    data testdata type ref to test_data.

    data mocked_get_objects type ref to zif_ca03_hr_get_objects.

    data mocked_basic type ref to zif_ca03_hr_basic.

    data mocked_reader type ref to zif_hr05_salhist_reader.

    methods mockup_get_objects

      returning value(result) like mocked_get_objects.

    methods mockup_basic

      returning value(result) like mocked_basic.

    methods mockup_reader

      returning value(result) like mocked_reader.

    methods setup_low.

    methods test_low.

  private section.

endclass.

class test implementation.

  method setup_low.

    mocked_basic = mockup_basic( ).

    mocked_reader = mockup_reader( ).

    mocked_get_objects = mockup_get_objects( ).

    iut = zcl_hr05_position_factory=>get_reader( ).

  endmethod.

  method mockup_basic.

    result ?= cl_abap_testdouble=>create( 'zif_ca03_hr_basic' ).

    cl_abap_testdouble=>configure_call( result

       )->returning( testdata->planning_variant ).

    result->get_active_planning_variant(  ).

    zcl_ca03_hr_injector=>inject_basic( result ).

  endmethod.

  "implement the rest of the test double mockups

  method test_low.

    cl_abap_unit_assert=>assert_equals(

      exp = testdata->expected_employees

      act = iut->read_employees( value #( ) ) ).

    cl_abap_testdouble=>verify_expectations( mocked_get_objects ).

  endmethod.

endclass.

At last, the concrete test class is relatively simple:

class test_unfiltered definition for testing risk level harmless duration short

                inheriting from test

                final.

  private section.

    methods setup.

    methods test for testing.

endclass.

class test_unfiltered implementation.

  method setup.

    testdata = new test_data_unfiltered( ).

    setup_low( ).

  endmethod.

  method test.

    test_low( ).

  endmethod.

endclass.

As you see, nothing changes comparing with the template coding. To create another test case, just copy the class, give it a different name and change the test data class that is being used:

class test_filtered definition for testing risk level harmless duration short

                inheriting from test

                final.

  private section.

    methods setup.

    methods test for testing.

endclass.

class test_filtered implementation.

  method setup.

    testdata = new test_data_filtered( ).

    setup_low( ).

  endmethod.

  method test.

    test_low( ).

  endmethod.

endclass.

No comments:

Post a Comment