ABAP Unit - TDF (Function Double)
In this article, let's take a look at how we can deal with function modules as dependencies without testing them as well.
Table of contents
It's been a while since the last article on unit testing. At that time we had actually already completed the series on the subject of automatic testing, but in the meantime SAP has made additional features available. There is now a test double for function modules, which we want to take a closer look at in this article.
Introduction
A test double is there to eliminate dependencies on called objects. This means that whenever a database is queried, a service called, another class started or a function module implemented, you get dependencies. Since you only want to check your own code in a unit test, such dependencies should disappear at test runtime. There is the concept of doubles for this, the dependency is exchanged and you get control over the return values. Tests are stable and repeatable again.
Preperation
For this we need a dependency, in this special case a function module. To do this, we create a test module that performs arithmetic operations. We only implement the operations for addition and subtraction:
FUNCTION z_bs_demo_test_function
IMPORTING
VALUE(id_input_one) TYPE i
VALUE(id_input_two) TYPE i
VALUE(id_method) TYPE string
EXPORTING
VALUE(ed_result) TYPE i.
CASE id_method.
WHEN '+'.
ed_result = id_input_one + id_input_two.
WHEN '-'.
ed_result = id_input_one - id_input_two.
ENDCASE.
ENDFUNCTION.
Configuration
First of all, we need a global level variable to hold our test environment. As for databases and CDS views, there is a separate environment:
CLASS-DATA:
go_function TYPE REF TO if_function_test_environment.
First of all we initialize the environment and transfer the function modules for which we want to create a test double.
" Initialize double
go_function = cl_function_test_environment=>create( VALUE #( ( 'Z_BS_DEMO_TEST_FUNCTION' ) ) ).
Next we get the double that we want to configure, this is done via the name of the function module.
" Get double for function module
DATA(lo_demo_function) = go_function->get_double( 'Z_BS_DEMO_TEST_FUNCTION' ).
Then we create the input configuration, this set defines a state. If all parameters are passed as defined when calling the function module, then this set is triggered.
" Configure input configuration
DATA(lo_input_config) = lo_demo_function->create_input_configuration(
)->set_importing_parameter( name = 'ID_INPUT_ONE' value = 10
)->set_importing_parameter( name = 'ID_INPUT_TWO' value = 10
)->set_importing_parameter( name = 'ID_METHOD' value = '*'
).
Matching the input configuration, an output configuration must now also be defined. This provides the output parameters of the function module with values.
" Configure output configuration
DATA(lo_output_config) = lo_demo_function->create_output_configuration(
)->set_exporting_parameter( name = 'ED_RESULT' value = 100
).
In the last step, the two configurations must now be brought together. For this we configure the call, if the input values correspond to the default, then the output should be set according to the default.
" Set matching configuration
lo_demo_function->configure_call( )->when( lo_input_config )->then_set_output( lo_output_config ).
Possibilities
In addition to being able to set the output directly, you can also simulate other behavior, such as the output of classic exceptions, OO exceptions or a direct response. The double behaves like the test double for classes. You can get more information in the interface:
Test case
Let's now define a first abstract test case to test this behavior. To do this, we call the function module directly in our test case. Normally we would test the global class and actually want to deactivate the function module. To illustrate, let's just test the function module once to test the actual double:
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 10
id_input_two = 10
id_method = '*'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 100 ).
We create the other test cases (see complete example), whereby one test case should not work. Here we do not create any parameters for a combination. This should not find any configuration. This means that scenarios that were actually not defined in the function module at all also work:
Full example
As always, the entire example of the global test class with the appropriate comments and definitions:
"! @testing Z_BS_DEMO_TEST_FUNCTION
CLASS zcl_bs_demo_funcdouble DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
CLASS-DATA:
go_function TYPE REF TO if_function_test_environment.
CLASS-METHODS:
class_setup,
class_teardown.
METHODS:
not_defined_method FOR TESTING,
wrong_substraction FOR TESTING,
not_defined_case FOR TESTING.
ENDCLASS.
CLASS zcl_bs_demo_funcdouble IMPLEMENTATION.
METHOD class_setup.
go_function = cl_function_test_environment=>create( VALUE #( ( 'Z_BS_DEMO_TEST_FUNCTION' ) ) ).
DATA(lo_demo_function) = go_function->get_double( 'Z_BS_DEMO_TEST_FUNCTION' ).
" Configure test double - First call
DATA(lo_input_config) = lo_demo_function->create_input_configuration(
)->set_importing_parameter( name = 'ID_INPUT_ONE' value = 10
)->set_importing_parameter( name = 'ID_INPUT_TWO' value = 10
)->set_importing_parameter( name = 'ID_METHOD' value = '*'
).
DATA(lo_output_config) = lo_demo_function->create_output_configuration(
)->set_exporting_parameter( name = 'ED_RESULT' value = 100
).
lo_demo_function->configure_call( )->when( lo_input_config )->then_set_output( lo_output_config ).
" Configure test double - Second call
lo_input_config = lo_demo_function->create_input_configuration(
)->set_importing_parameter( name = 'ID_INPUT_ONE' value = 20
)->set_importing_parameter( name = 'ID_INPUT_TWO' value = 10
)->set_importing_parameter( name = 'ID_METHOD' value = '-'
).
lo_output_config = lo_demo_function->create_output_configuration(
)->set_exporting_parameter( name = 'ED_RESULT' value = 11
).
lo_demo_function->configure_call( )->when( lo_input_config )->then_set_output( lo_output_config ).
ENDMETHOD.
METHOD class_teardown.
go_function->clear_doubles( ).
ENDMETHOD.
METHOD not_defined_method.
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 10
id_input_two = 10
id_method = '*'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 100 ).
ENDMETHOD.
METHOD wrong_substraction.
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 20
id_input_two = 10
id_method = '-'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 11 ).
ENDMETHOD.
METHOD not_defined_case.
DATA:
ld_result TYPE i.
CALL FUNCTION 'Z_BS_DEMO_TEST_FUNCTION'
EXPORTING
id_input_one = 15
id_input_two = 20
id_method = '+'
IMPORTING
ed_result = ld_result.
cl_abap_unit_assert=>assert_equals( act = ld_result exp = 35 ).
ENDMETHOD.
ENDCLASS.
Conclusion
Now it is also possible to easily test function modules in our objects and to do without test seams. Function modules will be used less and less in the future, but as long as they exist, there should also be a way to mock them.