ABAP Unit Test for Odata Services
Blogs in the Series
1 | Why OpenSAP Started ABAPUnitTest Course |
2 | ABAP Unit Test in Odata – Current One |
3 | ABAP Unit Test – Implementing TDD |
4 | ABAP Unit Test meets Legacy Code |
So continuing the ABAP Unit Test momentum from previous blog, we have managed to create ABAP Unit test for Odata services. The intent of this blog is to describe how to create local unit test classes for Odata services.
Example Considered
In order to keep things simple we have created an Odata service which returns the description of the document type (AUART from TVAKT table) field based on the code passed to it. So I want to test my filter functionality where we will Document type code and it shall return description. In case no description exists then ABAP Unit test shall fail.
Gateway Service created in SEGW with TVAKT table.
We used TVAKT table and created a service manually. Implemented the Get_Entityset method as shown below. The code simply returns the document type description based on the document type code passed to it.
METHOD tvaktset_get_entityset.
DATA:lt_filters TYPE /iwbep/t_mgw_select_option,
ls_filter TYPE /iwbep/s_mgw_select_option,
ls_val TYPE /iwbep/s_cod_select_option.
READ TABLE it_filter_select_options INTO ls_filter INDEX 1.
IF sy-subrc EQ 0.
READ TABLE ls_filter-select_options INTO ls_val INDEX 1.
SELECT * FROM tvakt INTO TABLE et_entityset WHERE auart EQ ls_val-low AND spras = 'E'.
ENDIF.
ENDMETHOD.
ABAP Unit Test Class Definition
So we started by creating the unit test class for *DPC_EXT in the eclipse.
This class has 3 methods and 3 attributes
Attributes
- CUT – referring to our DPC class
- MS_REQUEST_CONTEXT_STRUCT requesting structure to get context object
- MO_REQUEST_CONTEXT_OBJECT Object which will be passed to our DPC testing method.
Variable 2 and 3 were added after we faced an exception in our general flow explained below.
Methods
- SETUP for our CUT object creation
- GET_DOC_TYPE_TEXT for testing our different set of inputs.
- RUN_FILTER basically contains reusable code to which test parameters are passed by GET_DOC_TYPE_TEXT method.
CLASS ltc_znabheet_dpc_ext DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
DATA:
" Request Structure filled to get the corresponding request context.
ms_request_context_struct TYPE /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit,
" Request context which is normally passed to each method call
mo_request_context_object TYPE REF TO /iwbep/cl_mgw_request_unittst,
" DPC referring to our extension call
cut TYPE REF TO zcl_znabheet_dpc_ext.
METHODS setup.
METHODS run_filter
IMPORTING
im_auart TYPE string
im_entity TYPE string
im_entityset TYPE string.
METHODS get_doc_type_text FOR TESTING RAISING cx_static_check.
ENDCLASS.
ABAP Unit Test Class Implementation
In method GET_DOC_TYPE_TEXT we call our RUN_FILTER method for each pair of inputs. In RUN_FILTER method we first tried to fill the Filter internal table and call GET_ENTITYSET method with just filter table with a hope that it will return us the searched values. Sadly it did not work, we were thrown an exception.
CLASS ltc_znabheet_dpc_ext IMPLEMENTATION.
"Created our data provider
METHOD setup.
CREATE OBJECT cut.
ENDMETHOD.
METHOD get_doc_type_text.
" Run valid Test
run_filter( EXPORTING im_auart = 'AG' im_entity = 'TVAKT' im_entityset = 'TVAKTSet' ).
" Run Invalid test
"run_filter( EXPORTING im_auart = 'ZZ' im_entity = 'TVAKT' im_entityset = 'TVAKTSet').
ENDMETHOD.
METHOD run_filter.
DATA: lt_filter_select_options TYPE /iwbep/t_mgw_select_option,
ls_filter_select_option TYPE /iwbep/s_mgw_select_option,
lt_filter TYPE /iwbep/t_cod_select_options,
ls_filter TYPE /iwbep/s_cod_select_option,
lr_data TYPE REF TO data,
ls_context TYPE /iwbep/if_mgw_appl_srv_runtime=>ty_s_mgw_response_context.
FIELD-SYMBOLS: <tab> TYPE table.
" Fill the filter field detail
ls_filter-sign = 'I'.
ls_filter-option = 'EQ'.
ls_filter-low = im_auart.
APPEND ls_filter TO lt_filter.
ls_filter_select_option-property = im_auart.
ls_filter_select_option-select_options = lt_filter.
APPEND ls_filter_select_option TO lt_filter_select_options.
"read data
TRY .
cut->/iwbep/if_mgw_appl_srv_runtime~get_entityset(
EXPORTING
io_tech_request_context = mo_request_context_object
it_filter_select_options = lt_filter_select_options " Table of select options
IMPORTING
er_entityset = lr_data
es_response_context = ls_context
).
CATCH /iwbep/cx_mgw_busi_exception.
CATCH /iwbep/cx_mgw_tech_exception.
ENDTRY.
" If no data found for document type text then it is an exception
ASSIGN lr_data->* TO <tab>.
CALL METHOD cl_abap_unit_assert=>assert_not_initial
EXPORTING
act = <tab>. " Actual Data Object
ENDMETHOD.
ENDCLASS.
So that actually led us to something related to Request context parameter is missing. On googling found a SAP Help which actually talked unit test integration and implementing a method to get the request context
ABAP Unit Test Class – Method INIT_DP_FOR_UNIT_TEST
SAP has provided a method named INIT_DP_FOR_UNIT_TEST in all DPC_EXT classes to support unit testing. This method initializes the data provider instance or request context which we will be using to pass to our service call.
We implemented this method before the service call and Bingo everything was working fine both for positive as well as negative test cases. Revised RUN_FILTER method.
METHOD run_filter.
DATA: lt_filter_select_options TYPE /iwbep/t_mgw_select_option,
ls_filter_select_option TYPE /iwbep/s_mgw_select_option,
lt_filter TYPE /iwbep/t_cod_select_options,
ls_filter TYPE /iwbep/s_cod_select_option,
lr_data TYPE REF TO data,
ls_context TYPE /iwbep/if_mgw_appl_srv_runtime=>ty_s_mgw_response_context.
FIELD-SYMBOLS: <tab> TYPE table.
" Fill the data set and entity names to be unit tested
ms_request_context_struct-technical_request-source_entity_type = im_entity.
ms_request_context_struct-technical_request-target_entity_type = im_entity.
ms_request_context_struct-technical_request-source_entity_set = im_entityset.
ms_request_context_struct-technical_request-target_entity_set = im_entityset.
" Fill the filter field detail
ls_filter-sign = 'I'.
ls_filter-option = 'EQ'.
ls_filter-low = im_auart.
APPEND ls_filter TO lt_filter.
ls_filter_select_option-property = im_auart.
ls_filter_select_option-select_options = lt_filter.
APPEND ls_filter_select_option TO lt_filter_select_options.
" Every DPC class now has INIT_DP_FOR_UNIT_TEST method which is used to provide a data
" instance which can be used for unit testing
mo_request_context_object = cut->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test(
is_request_context = ms_request_context_struct
).
"read data
TRY .
cut->/iwbep/if_mgw_appl_srv_runtime~get_entityset(
EXPORTING
io_tech_request_context = mo_request_context_object
it_filter_select_options = lt_filter_select_options " Table of select options
IMPORTING
er_entityset = lr_data
es_response_context = ls_context
).
CATCH /iwbep/cx_mgw_busi_exception.
CATCH /iwbep/cx_mgw_tech_exception.
ENDTRY.
" If no data found for document type text then it is an exception
ASSIGN lr_data->* TO <tab>.
CALL METHOD cl_abap_unit_assert=>assert_not_initial
EXPORTING
act = <tab>. " Actual Data Object
ENDMETHOD.
Learning
In order to implement ABAP unit test, method INIT_DP_FOR_UNIT_TEST must be called to get the request context.I hope this basic blog will help in implementing local test classes for all Odata services. Feel free to provide your feedback.
OK - this is another one I'm going to add to our system.
Thank you for sharing it!
Michelle
Hello Nabheet,
Thanks for this blog and for the motivation to jump into TDD when developing OData services.
I would just like to add to everyone interesting in this topic that SAP provides a framework to automate tests in our OData developements.
This framework is ECATT_ODATA, and it should help us definining automated tests for the OData services. I have tryed to explore a litlle but the tool, but I confess it's not simple to use. With the tool we get a wizard to create the tests with several options, but to my understaing the tool will generate the skeleton of the test classes where we should go there and add the testing code.
The test clasees are global and they use the adition of FRIENDS to be able to access the *DPC_EXT and MPC_EXT clases.
After some investigation I was not really convinved by the tool but nevertheless it's good to know it exists.
If anyone has some good experience with this tool please share with us.
Again, great work Nabheet. and I prefer your solution for doing Unit Testing in OData development.
Thank you
Sérgio Fraga
Thanks for the feedback Sergio Fraga.This is what i like about blogging by sharing my thoughts about unit test script i came to know ECATT_Odata from you which am not aware. Let me try to understand the framework and how it works. Lets keep learning and keep sharing our experience does not matter how small they are.
Hello again Nabheet Madan ,
I am using your approach with one of my OData developments and now I can test my code without using Gateway Client transaction!
Using Unit Tests give me the confidance that the code is working as expected and it's much more fun to work like this.
Thanks again for your investigation.
I would just like to refer that you have a bug in your code.
When you fill the PROPERTY of your LT_FILTER_SELECT_OPTIONS in your test code, your are passing the value of the import parameter IM_AUART.
The test passes because in your productive code your are always reading the first line of the filter, and in your example it's OK because you only have 1 filter, but if you had 2+ filters you should look at the filter PROPERTY, which in your case should be 'AUART', since it's the value you have defined as the ABAP field in your SEGW project.
Just a remark to make your code bullet proof 🙂
I would also suggest to create a repo in abapGit with your example so that anyone could pull the code and test it right away. abapGit is an amazing project and we should make use of it.
Have a great day
Sérgio Fraga
Thanks Sergio for the feedback. Agreed was trying to demonstrate the MVP in quick time so missed it will update.
One small request can you please all share your experience of TDD where you have used it will help us?
Thanks
Nabheet
Hello again,
My biggest advice is to read the book from Kent Beck, the father of TDD: Test Driven Development by Example
Regarding OData Test Units, I am having a problem and I would like to ask you if you know how to solve it.
In your example, in the productive code you are using the table IT_FILTER_SELECT_OPTIONS. This table comes with the properties filled with the names of the entity properties defined in the SEGW project:
Normally, when we use filters in OData Development, we should use the following method to extract the filters used in the request. This method will return a table with the ABAP Fields in the property and not with the names:
The problem I am having is that if I use the method above to extract the filters from the request, when I am running the Unit Test I will have a dump, because in the get_filter( ) method there is a call to the get_entity_type() method of the service model, and it seems that in Unit Test context, the service model is not instantiated.
If you change your productive code to call the get_filter_select_options() instead of using IT_FILTER_SELECT_OPTIONS you will get the same issue.
Does anyone have an ideia on how to solve this?
Currently I am using the IT_FILTER_SELECT_OPTIONS and instead of using the ABAP field names to extract the correct filters from this table I am using the Entity Property names but I think we should always work with the ABAP field names since we are working in the backend service.
Thanks a lot for any suggestion
Sérgio Fraga
Sergio, it seems like the ms_request_context_struct which is of type
TYPE /IWBEP/CL_MGW_REQUEST_UNITTST=>TY_S_MGW_REQUEST_CONTEXT_UNIT
has the ability to set the parameters.
```
" Every DPC class now has INIT_DP_FOR_UNIT_TEST method which is used to provide a data " instance which can be used for unit testing mo_request_context_object = cut->/iwbep/if_mgw_conv_srv_runtime~init_dp_for_unit_test( is_request_context = ms_request_context_struct ).
```
Hi Siergo,
Redefine your /IWBEP/IF_MGW_CONV_SRV_RUNTIME~INIT_DP_FOR_UNIT_TEST method in DPC_EXT with below code, then it will work.
/ DATA: lr_request_context TYPE REF TO /iwbep/cl_mgw_request_unittst=>ty_s_mgw_request_context_unit.
DATA: lt_headers TYPE tihttpnvp.
DATA: lo_context TYPE REF TO /iwbep/if_mgw_context.
DATA: lo_logger TYPE REF TO /iwbep/cl_cos_logger.
DATA: lo_msg_container TYPE REF TO /iwbep/if_message_container.
DATA: lo_msg_container_fw TYPE REF TO /iwbep/cl_mgw_msg_container.
DATA lo_model TYPE REF TO /iwbep/if_mgw_odata_fw_model.
*--- INIT_DP_FOR_UNIT_TEST redefined to create model
DATA(lo_metadata_provider) = /iwbep/cl_mgw_med_provider=>get_med_provider( ).
TRY.
lo_model ?= lo_metadata_provider->get_service_metadata(
iv_internal_service_name = <ODATA SERVICE NAME = *_SRV>
iv_internal_service_version = <ODATA VERSION NO>
).
CATCH /iwbep/cx_mgw_med_exception.
RETURN.
ENDTRY.
ASSERT mr_request_details IS INITIAL.
GET REFERENCE OF is_request_context INTO lr_request_context.
ro_request_context = NEW /iwbep/cl_mgw_request_unittst(
it_headers = lt_headers
io_model = lo_model ).
ro_request_context->set_request_context( ir_request_context = lr_request_context ).
lo_logger = /iwbep/cl_cos_logger=>get_logger( ).
lo_msg_container = /iwbep/cl_mgw_msg_container=>get_mgw_msg_container( ).
TRY.
lo_msg_container_fw ?= lo_msg_container.
lo_msg_container_fw->reset( ).
CATCH cx_sy_move_cast_error.
CLEAR lo_msg_container.
ENDTRY.
lo_context ?= NEW /iwbep/cl_mgw_context( ).
lo_context->set_parameter(
iv_name = /iwbep/if_mgw_context=>gc_param_msg_container
iv_value = lo_msg_container
).
lo_context->set_parameter(
iv_name = /iwbep/if_mgw_context=>gc_param_logger
iv_value = lo_logger
).
mo_context = lo_context.
Passing SERVICE_DOC_NAME and VERSION in /IWBEP/CL_MGW_REQUEST_UNITTST=>TY_S_MGW_REQUEST_CONTEXT_UNIT doesn't always seem to work.
Assuming that the test class is a friend of your *DPC_EXT class.
We've to set two parameters for the mo_model to be initialized.
After the call to
We've to set the service name and version as below
NOTE: This way of writing OData AUnits is now obsolete (I think as of 2022) and you should consider using Local Client Proxy if you are on higher version of ABAP Stack.
Hi,
does this also work for oData-Services that are generated based on corresponding CDS Views? The mid and long term way of implementing an oData service is potentially no longer manually coding via SEGW but modelling a corrsponding CDS View with the oData Annotation.
BR,
Holger
Dear Holger
We have two options here as per my understanding as i dont see any annotation provided in CDS which actually make sense.
Thanks
Nabheet
Well,
From the open SAP course "Writing Testable Code for ABAP" we discovered that SAP is delivering a ABAP CDS Test Double Framework 🙂
Thanks for pointing to that course. Nice to see info about all the test doubles. It's one way that the io_tech_request_context might be able to be mocked.
Hi Nabheet,
Thank you for this excelent post!
Can you atach a test example for "get_entity" implementation method? I don't know how to set the key.
Thank you!!
Hi Nabheet,
great work 🙂 I have another question: Do you see a chance to test the MPC_EXT class as well? The use case is to to test a redefinition of the define method.
Best Regards,
Tobias
Hello.
Your odata method has an exporting parameter er_entity_set = lr_data. The type of this element is 'data'. Do you know how you can evaluate the content of the elements and properties of these elements ?
I mean something like assertEquals( exp = 'Man' act = lr_data-gender ). Is there any way of casting or converting that can be used ?
Best Regards
Koen
Hello Nabheet.
Thank you for this example. I have a similar case where I want to pass filter parameters to the get_entityset method. The only difference is that I want to use the get_filter method of io_tech_request_context instead of it_filter_select_options.
In the get_entityset method I use the following code to get the filters from the request:
So in the unit test I want to pass the filter like this:
When I run the unit test, I get the following output (the messages are partly German):
Stack:
How can I work in unit tests with the technical request context and filters?
Kind regards
Andreas
For those who have the same problem.
Do not set references in ms_request_context_struct before the method init_dp_for_unit_test is called. Set them afterwards. The request initialization process includes some XSL transformation which can not deal with reference types.
The references in the structure are:
The class /iwbep/cl_mgw_request_unittst have separate public methods for those parameters except the last one (key_converter).
Sample:
Unfortunately, /iwbep/cl_mgw_request_unittst doesn’t work with filter objects even though it has the setter for them (check the post above by Sergio Fraga).
The possible option here is to use a wrapper class around /iwbep/cl_mgw_request_unittst to be able to redefine the method /iwbep/if_mgw_req_entityset~get_filter which will return the filter objects you wanted. And the filter objects themselves should be test doubled based on the interface /iwbep/if_mgw_req_filter as the standard filter implementation actively uses service model objects (incl. convertions etc.) which aren’t available in the context of this tests.
Does these tests use mock data?
Hi Nabheet Madan ,
Thank you for sharing this blog, interesting. It really inspired me to think about unit tests for ODATA context. Earlier I tried to mock classes, it was too much work and I gave up. Now you show an easier way, great!
This framework however was still too heavy for me as it requires extra development if we want to test complex filters, selects, and other elements of ODATA. So I built my own simple way of testing ODATA by entering ODATA URL directly.
Let's share experience! My way for ODATA testing can be found in the blog here:
https://blogs.sap.com/2021/09/23/abap-unit-tests-for-odata-requests/
Cheers!
Adam