Skip to Content
Technical Articles

Separate data classes in unit tests

In my last post, 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.

Summary

As in production code, in test code the use of abstract classes can be useful. It permits to put repetitive coding in one place and reuse it in each concrete case.

By its very nature, the generation of test data often generates a large number of lines of code. Separating it in specialized classes can keep the coding more readable. And using a class hierarchy prevents us from doubling code as well.

Sometimes it even makes sense to extract the data classes into global (test) classes, so you can reuse them in unit tests for other classes. This is mainly useful for multilevel tests where you want to use the same data in different levels of testing.

2 Comments
You must be Logged on to comment or reply to a post.
  • I really like the idea and also the explanations. I used a similar approach by trying out local and public abstract classes to structure my data. There is also the Tcode SECATT in which you can store test data. Some colleagues of me prefer that way but I like to have the data available in Eclipse as well