Become Function Module Independent ft. ABAP Units
In pursuit of making ABAP Unit Test less dependent and more independent 😊, today I have decided to write on an interesting but less explored topic which is removing function module dependency in ABAP Unit Test using TDF. This is one of the less discussed topics in abap unit test double framework.After implementing some of them and in absence of as many resources as other topics, I have decided to share my experience with a sample example and put an overview in one place.
So while implementing ABAP Units, we may have come across scenarios where the Depended-On-Componen(DOC) is a function module.
So how do you mock that and remove that dependency.Let’s see that with an example:
For the sake of demo I have created a sample function module which takes as input a document number and returns the number of items it has ( please don’t go by the use case as more focus is on the approach 😊)
I have created a method in a sample class which uses the function module and has some processing logic based on the function module’s output.
class ZCL_TEST_FM_DEMO definition public final create public . public section. methods METHOD_UNDER_TEST importing !IV_EBELN type EBELN exporting !EV_ITM_COUNT type I . PROTECTED SECTION. PRIVATE SECTION. ENDCLASS. CLASS ZCL_TEST_FM_DEMO IMPLEMENTATION. METHOD METHOD_UNDER_TEST. IF iv_ebeln IS NOT INITIAL. CALL FUNCTION 'ZFM_DOC_GET_COUNT' EXPORTING ebeln = iv_ebeln IMPORTING count = ev_itm_count. ENDIF. "Further Processing Logic based on the item count ENDMETHOD. ENDCLASS.
Now in the test class I would make use of Test Double Framework specific to Function Modules to mock this FM and henceforth call the mocked instance instead of the productive one.
For this I have declared an environment variable which refers to the test double framework interface.
CLASS-DATA: fm_environment TYPE REF TO if_function_test_environment.
Then in Class-Setup method I create an instance and get the test double object.
fm_environment = cl_function_test_environment=>create( VALUE #( ( |ZFM_DOC_GET_COUNT| ) ) ). DATA(fm_double) = fm_environment->get_double( |ZFM_DOC_GET_COUNT| ).
It’s time to set the input and output parameters.I create that using the create_input_configuration method and simultaneously the create_output_configuration method respectively.These configuration instances mock the actual import and export parameters of the function module.
DATA(input_data) = fm_double->create_input_configuration( )->set_importing_parameter( name = |ebeln| value = |5500000000| ). DATA(output_data) = fm_double->create_output_configuration( )->set_exporting_parameter( name = |count| value = 2 ).
Finally I configure my test double to set the output when the same input is provided.In other words, I set the input and output expectations.
fm_double->configure_call( )->when( input_data )->then_set_output( output_data ).
That’s it as far as mocking was concerned
Now as usual, in my unit test method I call the method under test as usual and create positive and negative scenarios.
METHOD determine_target_amount. CONSTANTS: cv_ebeln_pos TYPE ebeln VALUE '5500000000', cv_ebeln_neg TYPE ebeln VALUE '5500000001'. "positive f_cut->method_under_test( EXPORTING iv_ebeln = cv_ebeln_pos IMPORTING ev_itm_count = DATA(lv_itm_count) ). cl_abap_unit_assert=>assert_equals( act = lv_itm_count exp = 2 msg = 'item count matched' ). "negative f_cut->method_under_test( EXPORTING iv_ebeln = cv_ebeln_neg IMPORTING ev_itm_count = lv_itm_count ). cl_abap_unit_assert=>assert_initial( act = lv_itm_count msg = 'item count is initial' ). ENDMETHOD.
What would happen is on the call of the function module, the test double framework generated function module will be called instead of the productive function module and you have the data in your control.This would make the test cases consistent across all environments and eliminate AUnit failures
Some additional points to remember:
- You can also set Tables and Changing parameters in the same way as exporting parameters
- Similarly, you can also set multiple importing, tables, changing and exporting parameters
- In the configure_call( ) method, you can also raise exceptions and ignore all input parameters and only set the output
Hope this resource was useful to some extent in bringing some relevant light on mocking Function Modules in ABAP Unit Test Classes.
Do post your comments, queries and suggestions if any on the same.
I am also available at LinkedIN at https://www.linkedin.com/in/iheartsap/ for any queries.
Thank for sharing this info.
Do you know as of which ABAP release this feature is available?
I've checked our ABAP 7.55 system, and class CL_FUNCTION_TEST_ENVIRONMENT is not available there 🙁
Firstly thank you so much for reading the content.
It's available as of Release 7.56 😐
Thanks for the info.
I was aware of this. I cannot really see the benefit compared to wrapping the function module in a method which is accessed via an interface and can thus have a test double. In both cases you are hard coding the input/output values.
If anything I was concerned that a function module test double framework might encourage people to keep creating new function modules when really - since the year 2000 - function modules are obsolete except for certain cases which OO ABAP cannot do e.g. RFC calls, calling a DYNPRO screen. And in both of those cases the function module should have no business logic, just outsource such logic to one or more classes.
A-ha! You will say - what if I need a standard SAP function module? Well, you could wrap that module in a class but the real question is, after 22 years why SAP not eaten their own dog food and stopped the SAP standard code being dependant on function modules?
Me either. I wrap by external dependencies for a program in their own interface/class.
I don't like using the cl_function_test_environment etc. classes. It seems highly risky. A programmer in the future adding a function module somewhere in the code will have to know that they need to add that FM to the test environment. While with a separate class they'll also have to know that the FM call goes there, at least if they get that right you don't have to change the test code.
I'd only use these classes to get legacy code into a test harness.