Search code examples
unit-testingdependency-injectionstatic-methodsabapinterpreted-language

Dependency injection to class-method in ABAP


I have a scenario where my class method (A) calls another class method (B). So A is depended on B. I’d like to get rid of the dependency to be able to run unit tests. Decoupling and dependency injection are somewhat based on instantiation. But class methods (static methods) don’t need an instance by nature. I’ve already made two solutions working, but none of them seems 100% for me:

  1. We create instance of class B (production code) or instance of the test double (for unit test). We inject it as a parameter to the class method under test. The inner class method is called on the injected instance, not on the class.

I don’t like we need to make an instance of class, although we are using class method. It can take time. It needs a bit more code.

  1. We inject actual class name as a string parameter and we use dynamic CALL METHOD.

As I am not fan of interpreted languages, I consider this a mess that can bring serious runtime problems. Since we do this to implement unit tests and consequently to eliminate possible bugs; using dynamic calls seems counterproductive. Also it is painful to work with parameters.

Is there another way to solve this? Is there some important point I missed?

Bellow, there are key parts of both solutions. There are not essential to understand the problem, but they might help.

1)

INTERFACE lif_readfile.
  CLASS-METHODS gui_upload
    IMPORTING file_path TYPE string
    CHANGING data_tab  TYPE truxs_t_text_data.
ENDINTERFACE.

CLASS lcl_file_operations DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS:
      get_file_length
        IMPORTING
          !file_path         TYPE string
        CHANGING
          !filereader        TYPE REF TO lif_readfile OPTIONAL
        RETURNING
          VALUE(text_length) TYPE i.
ENDCLASS.

CLASS lcl_file_operations IMPLEMENTATION.
  METHOD get_file_length.

*create instance of default productive class
    IF filereader IS NOT BOUND.
      filereader = NEW lcl_readfile( ).
    ENDIF.

*use instance to call class method
    filereader->gui_upload(
      EXPORTING file_path = file_path
        CHANGING data_tab = lt_data
    ).

*code under test here..

  ENDMETHOD.
ENDCLASS.

2)

CLASS lcl_file_operations DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS:
      get_file_length
        IMPORTING
          !file_path         TYPE string
          !classname         TYPE string DEFAULT 'LCL_READFILE'
        RETURNING
          VALUE(text_length) TYPE i.
ENDCLASS.

CLASS lcl_file_operations IMPLEMENTATION.
  METHOD get_file_length.

*parameter definition
    ptab = VALUE #( ( name  = 'FILE_PATH'
                      kind  = cl_abap_objectdescr=>exporting
                      value = REF #( file_path ) )
                    ( name  = 'DATA_TAB'
                      kind  = cl_abap_objectdescr=>changing
                      value = REF #( lt_data ) ) ).

    DATA(meth)     = 'LIF_READFILE~GUI_UPLOAD'.

*dynamic call
    CALL METHOD (classname)=>(meth) PARAMETER-TABLE ptab.

*code under test here..

  ENDMETHOD.
ENDCLASS.

Solution

  • I’ve found so far two solutions that appears better to me. But if you know even a better solution; I am still looking forward to try it.

    Factory+injector (dependency lookup)

    Improved solution 1 – handling of instances is moved to the factory. Code for the factory and injector is not provided – it is standard solution.

    CLASS lcl_file_operations DEFINITION.
      PUBLIC SECTION.
        CLASS-METHODS:
          get_file_length
            IMPORTING
              !file_path         TYPE string
            RETURNING
              VALUE(text_length) TYPE i.
    ENDCLASS.
    
    
    CLASS lcl_file_operations IMPLEMENTATION.
      METHOD get_file_length.
    
        myfactory=>get_filereader( )->gui_upload(
          EXPORTING file_path = file_path
            CHANGING data_tab = lt_data
        ).
    
    *code under test here..
    
      ENDMETHOD.
    ENDCLASS.
    

    Pros

    • Cleaner and shorter code under test. Instance is created and tested somewhere else.

    Cons

    • It still creates instances.
    • More code in total.

    Using TEST-SEAM and TEST-INJECTION

    CLASS zcl_file_operations IMPLEMENTATION.
      METHOD get_file_length.
    
       TEST-SEAM seam_gui_upload.
         zcl_filereader=>gui_upload(
            EXPORTING file_path = file_path
            CHANGING data_tab = lt_data
         ).
       END-TEST-SEAM.
    
    *code under test here..
    
      ENDMETHOD.
    ENDCLASS.
    

    FOR TESTING method

    *...
    TEST-INJECTION seam_gui_upload.
     ztc_testdouble=>gui_upload(
        EXPORTING file_path = file_path
        CHANGING data_tab = lt_data
     ).
    END-TEST-INJECTION.
    *...
    

    Pros

    • This seems like the best solution so far.
    • Using class-methods; no instances created.
    • Shortest code in total.

    Cons

    • Considered dirty technique by community (recommended only for legacy code).
    • Slight pollution of code under test.

    Note