ABAP Unit - Testable Code (Part 3)
Here we take a closer look at the options for deactivating dependent components in your own coding during the test period.
Table of contents
This article is about the methodology of dependency injection to deactivate components used during the test and thus to make your own code testable.
Term
This is about the technology of how to deal with dependent components and give the tester or caller the option of deactivating or overwriting such components.
Here is a “bad” example in which the caller hardly has a chance to deactivate the dependent component. The object is instantiated in the class and used immediately afterwards:
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
ENDMETHOD.
METHOD call_selector.
NEW zcl_test_doc( )->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
Using this example, you will now learn different methods of how you can replace the component with your own component. Here we will use a test double.
Preparation
In order for the strongly coupled class to be decoupled in the method, the class has to be rebuilt a bit. The instance variable should be swapped out as a member variable (attribute of the class) and the instantiation should be separated from the use accordingly. After the conversion, the class would look like this:
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string.
PRIVATE SECTION.
DATA:
mo_doc TYPE REF TO zif_test_doc.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
mo_doc = NEW zcl_test_doc( ).
ENDMETHOD.
METHOD call_selector.
mo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
We also create a dummy implementation in the local area, which exchanges the DOC and makes it available according to our logic. The name “LTD_” (Local Test Double) already shows you what the purpose of the class is.
CLASS ltd_doc DEFINITION FINAL.
PUBLIC SECTION.
INTERFACES: zif_test_doc.
ENDCLASS.
CLASS ltd_doc IMPLEMENTATION.
METHOD zif_test_doc~do_stuff_and_select_data.
" Empty implementation
ENDMETHOD.
ENDCLASS.
Constructor Injection
We offer an optional parameter via the constructor that overwrites the DOC. If we do not pass our own object, a corresponding instance of the concrete class is created.
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING
io_doc TYPE REF TO zif_test_doc OPTIONAL,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA:
mo_doc TYPE REF TO zif_test_doc.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
mo_doc = io_doc.
IF mo_doc IS NOT BOUND.
mo_doc = NEW zcl_test_doc( ).
ENDIF.
ENDMETHOD.
METHOD call_selector.
mo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
The test class with the application of the method now looks like this:
CLASS ltc_constructor_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS ltc_constructor_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( lo_local_doc ).
DATA(ld_result) = lo_cut->call_selector( abap_false ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
When generating the CUT, we can now transfer our own test double and thus control the behavior of our codes during the test.
Setter Injection
The class provides its own setter method which can be used to replace the DOC. This method can then be called before using the CUT.
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
RETURNING VALUE(rd_result) TYPE string,
set_component
IMPORTING
io_doc TYPE REF TO zif_test_doc.
PROTECTED SECTION.
PRIVATE SECTION.
DATA:
mo_doc TYPE REF TO zif_test_doc.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
mo_doc = NEW zcl_test_doc( ).
ENDMETHOD.
METHOD call_selector.
mo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
METHOD set_component.
mo_doc = io_doc.
ENDMETHOD.
ENDCLASS.
The test class with the application of the method now looks like this:
CLASS ltc_setter_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS ltc_setter_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( ).
lo_cut->set_component( lo_local_doc ).
DATA(ld_result) = lo_cut->call_selector( abap_false ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Parameter Injection
The parameter injection works with an optional transfer parameter when the method is called. If an instance is transferred, then this is used, otherwise a new instance of the used class is simply created.
CLASS zcl_test_handle_docs DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor,
call_selector
IMPORTING
id_flag TYPE abap_bool
io_doc TYPE REF TO zif_test_doc OPTIONAL
RETURNING VALUE(rd_result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_test_handle_docs IMPLEMENTATION.
METHOD constructor.
ENDMETHOD.
METHOD call_selector.
DATA(lo_doc) = CAST zif_test_doc( COND #(
WHEN io_doc IS BOUND
THEN io_doc
ELSE NEW zcl_test_doc( )
) ).
lo_doc->do_stuff_and_select_data( id_flag ).
ENDMETHOD.
ENDCLASS.
The test class with the application of the method now looks like this:
CLASS ltc_parameter_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS ltc_parameter_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( ).
DATA(ld_result) = lo_cut->call_selector(
id_flag = abap_false
io_doc = lo_local_doc ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Backdoor Injection
With backdoor injection, nothing in the global class has to be adjusted. The backdoor is generated via the FRIEND addition. The test class with the application of the method now looks like this:
CLASS ltc_backdoor_injection DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
get_data FOR TESTING.
ENDCLASS.
CLASS zcl_test_handle_docs DEFINITION LOCAL FRIENDS ltc_backdoor_injection.
CLASS ltc_backdoor_injection IMPLEMENTATION.
METHOD get_data.
DATA(lo_local_doc) = NEW ltd_doc( ).
DATA(lo_cut) = NEW zcl_test_handle_docs( ).
lo_cut->mo_doc = lo_local_doc.
DATA(ld_result) = lo_cut->call_selector( abap_false ).
cl_abap_unit_assert=>assert_initial( ld_result ).
ENDMETHOD.
ENDCLASS.
Usage
With the four injection mechanisms, you have a number of tools at your disposal that you can use during development. In the most common cases, the constructor and the backdoor injection are used because they promise the least amount of effort and are flexible for the test. It is only important to separate the creation from the use of the class.
If you have your own ideas for the injection mechanism, you can of course also use this. However, these well-known patterns are the most common.
Conclusion
There are various ways to deactivate dependent components, but there is always an option to make your code testable. When developing, always keep in mind that another developer may want to test or use your components as well.