Friday 24 September 2021

ABAP Unit Tests for ODATA requests

Introduction

As an SAP developer coming from the Java world, I always focus on Object Oriented ABAP and believe in the high value of unit testing. So the question came once, how should I test ODATA service?

In the past I tried to build mocking classes, so that I could call the ZCL*DPC_EXT methods, but it was too difficult and had little value when compared to development efforts. Then I left the topic and decided to keep example requests in SAP Gateway Client through transaction /IWFND/GW_CLIENT. That was ok for documenting purposes, but it was not a unit test unfortunatelly.

ABAP Unit Tests for ODATA

Custom utility method for ODATA request

It is a clear recipe for how to test ODATA service and it works. But it was not suitable for me as I wanted to test different filters, expand, select options easily.

So again I did homework, debugged how SAP is running Gateway Client and built a simple utility method. It uses standard SAP methods. Few lines of code allows testing ODATA requests by exact string, like the client would call it. I think this is a simple but powerful way of testing requests, as we want to keep the syntax stable, especially when API was shared with client applications:

◉ It gives control and verifies that for a given request we get expected results and the API is stable.

◉ It quickly discovers if someone has changed parameter / entity name, so that it could have side effects for existing client applications. And if that is an expected change then unit tests must be also adjusted.

◉ It is a good documentation of how you can call the service and what are most common requests/use cases.

I am sharing the code which does the magic. I hope that at least one of you will take a chance to create automated unit tests for ODATA service by importing this code into your system.

CLASS-METHODS call_odata_request_json_format

        IMPORTING

          add_server_url       type abap_bool default abap_true

          odata_url            TYPE string

        EXPORTING

          VALUE(json_data)     TYPE any

          response_full_string TYPE string.

METHOD call_odata_request_json_format.

  CLEAR: json_data, response_full_string.

  DATA(final_odata_url) = odata_url.

  IF NOT ( final_odata_url CS '$format=json' ).

    IF ( final_odata_url CS '?' ).

      final_odata_url = final_odata_url && '&$format=json'.

    ELSE.

      final_odata_url = final_odata_url && '?$format=json'.

    ENDIF.

  ENDIF.

  DATA(request_header) = VALUE /iwfnd/sutil_property_t(

          ( VALUE #( name = '~request_method' value = 'GET' ) )

          ( VALUE #( name = '~request_uri' value = final_odata_url ) )

      ).

    DATA(http_caller) = /iwfnd/cl_sutil_client_proxy=>get_instance( ).

    http_caller->web_request(

      EXPORTING

        it_request_header     = request_header                 " HTTP Request Header - Table

      IMPORTING

        ev_status_code        = DATA(resp_status_code)         " HTTP Status Code

        ev_status_text        = DATA(resp_status_text)         " HTTP Status Text

        et_response_header    = DATA(resp_header)              " HTTP Response Header - Table

        ev_response_body      = DATA(resp_body)                " HTTP Response Body

    ).

    response_full_string = /iwfnd/cl_sutil_xml_helper=>transform_to_string( resp_body ).

    /ui2/cl_json=>deserialize(

      EXPORTING

        jsonx            = resp_body                 " JSON XString

      CHANGING

        data             = json_data                " Data to serialize

    ).

  ENDMETHOD.

Does it look complex? Maybe. But later you will see how easy it is to call it in a test code. Let’s explain sections quickly:

◉ Initially we verify if the query contains $format=json and if not we add it. We want to force json format so that result can be easily formatted to structure.

◉ request_header uses a specific format to pass the URL into web_request later. The URL can be relative, starting with /sap/opu/odata.

◉ web_request method is the core method that makes the ODATA request. More parameters can be added, but in my example the basic ones are imported.

◉ transform_to_string method converts bytes from response into readable text, containting the response content.

◉ /ui2/cl_json=>deserialize is used to parse the response JSON string into the structure. If the structure has matching fields to JSON structure, data is automatically populated. Note that parameter type is any so it is up to the calling program to pass the relevant type.

Creating unit test for ODATA request

Now it is time to see how the utility method can be used for test. I put it into ZCL_UTILS_UI5 class as a static method. Let’s assume that we have a model with a MaterialSet entity and $expand navigation to_ChangeLog, which allows us to retrieve ChangeLog data on demand.

METHOD test_odata_request.

    " Example of material entity type definition for json data mapping, entity found by key

    TYPES BEGIN OF type_json_resp_material.

    TYPES BEGIN OF d.

    INCLUDE TYPE zcl_material_mpc=>ts_zmaterial_ctype.

    " Now example of expand entity

    types to_changelog type zcl_material_mpc=>ts_zmaterial_change_record.

    TYPES END OF d.

    TYPES  END OF type_json_resp_material.

    " Example of material list type definition for json data mapping, entities found by filter

    TYPES BEGIN OF type_json_resp_material_list.

    TYPES BEGIN OF d.

    TYPES results TYPE STANDARD TABLE OF zcl_material_mpc=>ts_zmaterial_ctype WITH DEFAULT KEY.

    TYPES END OF d.

    TYPES  END OF type_json_resp_material_list.

    DATA json_response_entity TYPE type_json_resp_material.

    DATA json_response_list TYPE type_json_resp_material_list.

    DATA(odata_url) = |/sap/opu/odata/SAP/ZMATERIAL_SRV/MaterialSet('00000001')|.

    zcl_utils_ui5=>call_odata_request_json_format(

      EXPORTING

        odata_url            = odata_url

      IMPORTING

        json_data            = json_response_entity

        response_full_string = DATA(response_string)

    ).

    cl_abap_unit_assert=>assert_equals(

        act = json_response_entity-d-materialnumber

        exp = '00000001'

        msg = 'Material number not retrieved from ODATA request as expected'

   ).

    odata_url = odata_url && '?$format=json&$expand=to_ChangeLog'.

    zcl_utils_ui5=>call_odata_request_json_format(

      EXPORTING

        odata_url            = odata_url

      IMPORTING

        json_data            = json_response_entity

        response_full_string = DATA(response_string2)

    ).

   cl_abap_unit_assert=>assert_equals(

        act = json_response_entity-d-materialnumber

        exp = '00000001'

        msg = 'Adding $format=json should have same effect, as it is defaulted'

   ).

   cl_abap_unit_assert=>assert_equals(

        act = json_response_entity-d-to_changelog-materialnumber

        exp = '00000001'

        msg = 'Expand to_ChangeLog should also fill in subentity data'

   ).

Example above shows just the simple calls, but it can be easily extended with any ODATA query.

Many lines of code above are used for types definitions and we use MPC class for simplification. This is because we want to have data mapped automatically by the JSON framework into corresponding fields of structure. Then it is easy to do response data verification.

As an alternative we can also process the response_string which is a JSON formatted text response, useful for debugging where we see the conents of the ODATA response.

Additional notes

◉ Any ODATA $parameters can be easily added for test, like $filter, $expand, $skip, $filter – no limitation, simply define your query as you want it.

◉ Local types definitions can be set up just once on top of Unit Test class, so that it is reused across test methods.

◉ I think that unit tests can be created in DPC_EXT class as relevant to the ODATA service, or else a new custom business class can be used.

◉ Note that this is testing on a live database in development system and data can be changed in the future. Make sure that data exists in tables before you do assertion on ODATA response results!

Summary

Now I can verify my ODATA service with different variants in 2.6 seconds:

ODATA, SAP ABAP Prep, SAP ABAP Exam Prep, SAP ABAP Tutorial and Materials, SAP ABAP Career, SAP ABAP Certification, SAP ABAP Development, SAP ABAP Learning

There are many explanations why not to create Unit Tests. But if you create them, all in all there will be isolated areas of tested code which at the end become bigger tested areas.

SAP ODATA automated testing was too difficult for me to practice it. Now with the simple framework I can do it with little effort. I believe that these tests will be a good documentation and I try to test at least scenarios which are used by client applications.

Finally the power of unit tests is that they verify functionality within seconds and can be scheduled automatically if needed. If we practice them, they do not add time but make development faster, there are less defects in quality.

No comments:

Post a Comment