Technical Articles
SAP ABAP Unit test class using Interface injection
Introduction
Interface injection is a technique where one object supplies the dependencies data of another object. The intention behind dependency injection is to decouple the objects to the extent that no main code must be changed simply because a dependent object needs to be tested during UTC execution.
In this document we will discuss how to create Interface injection which is used to remove dependency from main CUT (Class under test) class.
Types of Interface injection:
- Setter level
- Constructor level
Approach
Interface injection is created mainly for local interface/instance. Global interface can be mocked using Test Double Framework. TEST_SEAM statement is used to block the dependent statement and TEST_INJECTION will replace the mocked code which is blocked inside TEST_SEAM. Local interface class will be local to the unit test class (UTC).
- Setter level:
Dependency on Interface method can be removed in Setter level dependency injection. If CUT class requires any dependent interface method from dependent class, then we will be creating interface injection at setter level. Block the dependent code using TEST_SEAM and create the required local instance of the dependent interface. This local interface method will be accessible within the unit test class.
Example:
Unit test class is created for ZCL_TEST_DEMO-GET_DATA method. GET_DATA will return RV_DATA by accessing dependent class interface /ZIF_DEP_TEST-GET_DEP_DATA method.
- CUT class : This is CUT method will return all the data from dependent data. TEST_SEAM is blocking interface object of ZCL_DEP_TEST class.
METHOD get_data.
DATA : lo_dep_obj TYPE REF TO /ZIF_DEP_TEST.
TEST-SEAM get_instance. “get_instance is test_seam unique name
lo_dep_obj = ZCL_DEP_TEST-GET_INSTANCE(). “Interface object is returned
END-TEST_SEAM.
DATA(lv_data) = lo_dep_obj->GET_DEP_DATA(). “Data is returned from dependent method
rv_data = lv_data.
ENDMETHOD.
- Interface injection: Local helper class ltc_help_dep_test is created for /ZIF_DEP_TEST interface. PARTIALLY IMPLEMENTATION keyword is used to implement only required method for our UTC’s for more information check blog – Use of “INTERFACE-PARTIALLY IMPLEMENTED”.
- Local helper class definition of ltc_help_dep_test.
CLASS ltc_help_dep_test DEFINITION. PUBLIC SECTION. INTERFACES: /ZIF_DEP_TEST PARTIALLY IMPLEMENTED. ENDCLASS.
- Local helper class implementation of ltc_help_dep_test.
CLASS ltc_help_dep_test IMPLEMENTATION. METHOD /ZIF_DEP_TEST~GET_DEP_DATA. RV_DATA = ‘TEXT’. “RV_DATA returning parameter of GET_DEP_DATA method ENDMETHOD. ENDCLASS.
- Local Test method : While testing GET_DATA method of CUT class, using the TEST_INJECTION local class instance will be injected to this dependent object lo_dep_obj. So, while executing UTC it will trigger local helper class (ltc_help_dep_test) instead of dependent interface. The object of local helper class ltc_help_dep_test will be used within local class.
When lo_dep_obj object is created with type ltc_help_dep_test then lo_dep_obj->GET_DEP_DATA() code from CUT method will trigger local helper class method and it will return RV_DATA = ‘TEXT’ to local test method.
METHOD ut_get_data.
TEST-INJECTION get_instance.
CREATE OBJECT lo_dep_obj TYPE ltc_help_dep_test. “Local interface object is created
END_TEST_INJECTION.
mo_cut->get_data(
RETURNING
RV_DATA = DATA(lv_data) "Test method is triggred though mo_cut
).
ENDMETHOD.
2. Constructor level:
If CUT class requires global attributes from dependent class, then we will create constructor level dependency injection. All mandatory global attributes data can be filled in the CONSTRUCTOR method of local helper class.
Example:
In this below example GET_DATA requires mv_set and mv_version global interface attribute of dependent class.
- CUT class: This is CUT method will return all the global data from dependent class. TEST_SEAM is blocking interface object of ZCL_DEP_TEST class.
METHOD get_data.
DATA :lo_dep_obj TYPE REF TO /ZIF_DEP_TEST.
TEST-SEAM get_instance. “get_instance is test_seam unique name
lo_dep_obj = ZCL_DEP_TEST-GET_INSTANCE(). “Instance of dependent class
END-TEST_SEAM.
DATA(lv_set) = lo_dep_obj->/ZIF_DEP_TEST~mv_set. “Global parameter of ZCL_DEP_TEST
DATA(lv_version) = lo_dep_obj->/ZIF_DEP_TEST~mv_version. “Global parameter of ZCL_DEP_TEST
DATA(lv_data) = lo_dep_obj->GET_DEP_DATA(). “Data is returned from dependent method
rv_data = lv_data.
ENDMETHOD.
- Interface injection: While local helper class definition we will define CONSTRUCTOR of /ZIF_DEP_TEST interface.
- Local helper class definition of ltc_help_dep_test.
CLASS ltc_help_dep_test DEFINITION.
PUBLIC SECTION.
INTERFACES: /ZIF_DEP_TEST PARTIALLY IMPLEMENTED.
METHODS : CONSTRUCTOR.
ENDCLASS.
2. Local helper class implementation of ltc_help_dep_test.
CLASS ltc_help_dep_test IMPLEMENTATION.
METHOD CONSTRUCTOR.
/ZIF_DEP_TEST ~mv_set = abap_true. “Global parameter of ZCL_DEP_TEST
/ZIF_DEP_TEST ~mv_version = ‘ADD’. “Global parameter of ZCL_DEP_TEST
ENDMETHOD.
ENDCLASS.
- Local Test method : Testing GET_DATA method of CUT class, using the TEST_INJECTION we will create object of local helper class ltc_help_dep_test. When lo_dep_obj object is created with type ltc_help_dep_test. The global interface attribute mv_set and mv_version is filled from the local helper class CONSTRUCTOR Global interface attribute can be filled by Constructor level injection.
METHOD ut_get_data.
TEST-INJECTION get_instance.
CREATE OBJECT lo_dep_obj TYPE ltc_help_dep_test. “Local interface object is created
END_TEST_INJECTION.
mo_cut->get_data( "Test method is triggred though mo_cut
RETURNING
RV_DATA = DATA(lv_data)
).
ENDMETHOD.
Conclusion:
Interface dependency injection is the best approach to create dependent interface objects and global interface attributes. This approach will provide more accuracy for the unit test class and helps developer to test their class without any dependency.
I do not belive that TEST-SEAMS were ever intended to be used in new code, only for really old code where it was deemed too difficult to untangle the dependencies.
Even then I still would not use them. they are an abomination for the following reasons:-
(a) the production code knows it can be tested as there are TEST-SEAMS in it
(b) There is nothing you can wrap in a TEST SEAM that cannot be abstracted away by wrapping it in a method call.
I would stronhly advise you to look up the open SAP course on unit testing, which covers many ways to do dependenecy injection. It too covered TEST-SEAMS as a last restort but did not encourage them.
There are so many better ways to do dependency injection. By far the best is the "dependency lookup" method.