Sebastian Wey,

ABAP Objects DI container Unit test

section>

In order to keep the effort for the development of unit tests manageable, aspects such as the single response principle or inversion of control are required in the OO design. For the latter in particular, a dependency injection container is an obvious choice in other environments such as Java or C#. In this blog post I would like to introduce a DI container for ABAP Objects that can be used for many simple scenarios.

For unit tests, it is important to design classes that are as independent of other classes as possible so that they can be tested in isolation. The use of interfaces is already a first step towards decoupling. But at the latest when an object of the dependent class is created, there is a dependency that leads to the problem in unit tests, that the dependent class cannot be replaced by a Mock. One solution is Dependency-Injection. The class to be tested may not create its dependent classes itself, but gets them injected, e. g. by passing parameters in the constructor.

The DI container presented in this blog post has the following features:

The DI container has two public methods:

An object for an interface? Let us assume that we have an interface for reading data from the SAP Business Partner. An implementing class is then registered for this interface, and when an object is requested for this interface, an object of the implementing class is created and returned. The user of the interface does not know the concrete class, and therefore has no dependency on this concrete class. For unit tests, a mock is now simply registered for this interface, so that the users of the interface can be tested without any data having to exist at all in the SAP Business Partner.

It becomes really interesting when the whole thing is done recursively. When the implementing class is created, the system checks whether it also has dependencies. The system checks whether the class has a constructor and, if so, whether it has object references as parameters. In this case, the system also attempts to create objects for the object references. The same check is also performed for these objects, and so on.

Source code

Here the complete source code of the class ZCL_DI_CONTAINER:

class ZCL_DI_CONTAINER definition
  public
  final
  create public .
public section.
*"* public components of class ZCL_DI_CONTAINER
*"* do not include other source files here!!!
  types T_SCOPE type CHAR1 .
  constants SCOPE_TRANSIENT type T_SCOPE value 'T'. "#EC NOTEXT
  constants SCOPE_SINGLETON type T_SCOPE value 'S'. "#EC NOTEXT
  methods BIND
    importing
      !SERVICE type CLIKE
      !TARGET type CLIKE optional
      !SCOPE type T_SCOPE default 'T' .
  methods GET
    importing
      !SERVICE type CLIKE
    returning
      value(RESULT) type ref to OBJECT .
protected section.
*"* protected components of class ZCL_DI_CONTAINER
*"* do not include other source files here!!!
private section.
*"* private components of class ZCL_DI_CONTAINER
*"* do not include other source files here!!!
  types:
    BEGIN OF t_binding,
      service       TYPE string,
      service_descr TYPE REF TO cl_abap_objectdescr,
      target_descr  TYPE REF TO cl_abap_classdescr,
      scope         TYPE t_scope,
      singleton     TYPE REF TO object,
    END OF t_binding .
  types:
    t_binding_tab TYPE HASHED TABLE OF t_binding WITH UNIQUE KEY service .
  data BINDINGS type T_BINDING_TAB .
  methods CREATE_BINDING
    importing
      !SERVICE_DESCR type ref to CL_ABAP_OBJECTDESCR
      !TARGET_DESCR type ref to CL_ABAP_CLASSDESCR optional
      !SCOPE type T_SCOPE default 'T'
    returning
      value(RESULT) type ref to T_BINDING .
  methods GET_CONSTRUCTOR_PARMBIND
    importing
      !TARGET_DESCR type ref to CL_ABAP_CLASSDESCR
    returning
      value(RESULT) type ref to ABAP_PARMBIND_TAB .
ENDCLASS.
CLASS ZCL_DI_CONTAINER IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_DI_CONTAINER->BIND
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVICE                        TYPE        CLIKE
* | [--->] TARGET                         TYPE        CLIKE (optional)
* | [--->] SCOPE                          TYPE        T_SCOPE (default ='T')
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD bind.
  DATA:
    service_descr TYPE REF TO cl_abap_objectdescr,
    target_descr  TYPE REF TO cl_abap_classdescr.
  service_descr ?= cl_abap_objectdescr=>describe_by_name( service ).
  IF NOT target IS INITIAL.
    target_descr ?= cl_abap_classdescr=>describe_by_name( target ).
  ENDIF.
  create_binding(
    service_descr = service_descr
    target_descr = target_descr
    scope = scope ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_DI_CONTAINER->CREATE_BINDING
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVICE_DESCR                  TYPE REF TO CL_ABAP_OBJECTDESCR
* | [--->] TARGET_DESCR                   TYPE REF TO CL_ABAP_CLASSDESCR (optional)
* | [--->] SCOPE                          TYPE        T_SCOPE (default ='T')
* | [<-()] RESULT                         TYPE REF TO T_BINDING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD create_binding.
  DATA binding TYPE t_binding.
  IF NOT target_descr IS INITIAL.
    binding-target_descr = target_descr.
  ELSE.
    binding-target_descr ?= service_descr.
  ENDIF.
  ASSERT binding-target_descr->is_instantiatable( ) EQ abap_true.
  binding-service = service_descr->absolute_name.
  binding-service_descr = service_descr.
  binding-scope = scope.
  INSERT binding INTO TABLE bindings REFERENCE INTO result.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_DI_CONTAINER->GET
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVICE                        TYPE        CLIKE
* | [<-()] RESULT                         TYPE REF TO OBJECT
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get.
  DATA:
    binding       TYPE REF TO t_binding,
    parmbindtab   TYPE REF TO abap_parmbind_tab,
    service_descr TYPE REF TO cl_abap_objectdescr.
  service_descr ?= cl_abap_objectdescr=>describe_by_name( service ).
  READ TABLE bindings REFERENCE INTO binding WITH TABLE KEY service = service_descr->absolute_name.
  IF sy-subrc NE 0.
    binding = create_binding( service_descr ).
  ENDIF.
  IF binding->scope EQ scope_singleton AND binding->singleton IS BOUND.
    result = binding->singleton.
    RETURN.
  ENDIF.
  parmbindtab = get_constructor_parmbind( binding->target_descr ).
  IF parmbindtab->* IS INITIAL.
    CREATE OBJECT result TYPE (binding->target_descr->absolute_name).
  ELSE.
    CREATE OBJECT result TYPE (binding->target_descr->absolute_name)
      PARAMETER-TABLE parmbindtab->*.
  ENDIF.
  IF binding->scope EQ scope_singleton AND NOT binding->singleton IS BOUND.
    binding->singleton = result.
  ENDIF.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_DI_CONTAINER->GET_CONSTRUCTOR_PARMBIND
* +-------------------------------------------------------------------------------------------------+
* | [--->] TARGET_DESCR                   TYPE REF TO CL_ABAP_CLASSDESCR
* | [<-()] RESULT                         TYPE REF TO ABAP_PARMBIND_TAB
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get_constructor_parmbind.
  DATA:
    constructor TYPE REF TO abap_methdescr,
    parmdescr   TYPE REF TO abap_parmdescr,
    refdescr    TYPE REF TO cl_abap_refdescr,
    dependency  TYPE REF TO cl_abap_objectdescr,
    parmbind    TYPE abap_parmbind.
  FIELD-SYMBOLS <fs> TYPE any.
  CREATE DATA result.
  READ TABLE target_descr->methods
      REFERENCE INTO constructor
      WITH KEY name = 'CONSTRUCTOR'.
  IF sy-subrc EQ 0.
    LOOP AT constructor->parameters REFERENCE INTO parmdescr.
      ASSERT parmdescr->type_kind EQ cl_abap_objectdescr=>typekind_oref.
      ASSERT parmdescr->parm_kind EQ cl_abap_objectdescr=>importing.
      refdescr ?= target_descr->get_method_parameter_type(
          p_method_name = constructor->name
          p_parameter_name = parmdescr->name ).
      CREATE DATA parmbind-value TYPE HANDLE refdescr.
      ASSIGN parmbind-value->* TO <fs>.
      dependency ?= refdescr->get_referenced_type( ).
      <fs> ?= get( dependency->absolute_name ).
      parmbind-name = parmdescr->name.
      parmbind-kind = cl_abap_objectdescr=>exporting.
      INSERT parmbind INTO TABLE result->*.
    ENDLOOP.
  ENDIF.
ENDMETHOD.
ENDCLASS.

The GET_CONSTRUCTOR_PARMBIND method is very important. The transfer parameters for the constructor are determined here, and recursion to resolve these parameters takes place here.

When registering classes, the TARGET type can be omitted. This allows classes to be registered on themselves. This can be used to register a class as Singleton. This means that this design pattern no longer has to be implemented in the class itself.

Usage

Classes are usually registered when a program is started, for example in the events LOAD-OF-PROGRAM of a function group or INITIALIZATION of a report, or in a SETUP method of a unit test class.

Example: Let us assume that there is an interface ZIF_BP_SERVICE with service methods for reading business partner data. There is also a class ZCL_BP_SERVICE, which implements this interface. There is also the interface ZIF_ACCOUNT and the class ZCL_ACCOUNT. In our program we register them like this:

LOAD-OF-PROGRAM.
  DATA di_container TYPE REF TO zcl_di_container.
  CREATE OBJECT di_container.
  di_container->bind( service = 'ZIF_BP_SERVICE' target = 'ZCL_BP_SERVICE' ).
  di_container->bind( service = 'ZIF_ACCOUNT' target = 'ZCL_ACCOUNT' ).

Class ZCL_ACCOUNT requires the BP service and therefore gets a corresponding parameter in its constructor:

CLASS zcl_account DEFINITION PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_account.
    METHODS constructor IMPORTING bp_service TYPE REF TO zif_bp_service.
* ...
ENDCLASS.

An object of the class ZCL_ACCOUNT is obtained in the following way:

DATA account TYPE REF TO zif_account.
account ?= di_container->get( service = 'ZIF_ACCOUNT' ).

Summary and Outlook

The presented DI container will certainly be sufficient for many simple scenarios. However, there are also scenarios in which more extensive requirements exist. If several implementations exist for an interface and the concrete implementation is to be selected using rules or filter values, multiple bindings are required. With complex class hierarchies, the limits of constructor injection are quickly reached. Setter injection, in which the dependencies are injected into the object using special methods, is a good solution here.

Translated with DeepL


Back to list