Application Development and Automation Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
Nihal__Raj
Explorer
1,474

Introduction:
AUnits are unit tests written by ABAP developers to test each functionality of their code. In RAP, AUnits are used to test functionalities such as validations, determinations, actions, and CRUD operations etc. Your AUnits should aim for maximum coverage of your code. It is recommended to follow TDD(Test Driven Development), where you write the test cases first and then implement the logic. This ensures that the test cases are based on the actual requirements and are not influenced by the implementation.

Reference link for TDD: https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide/test-driven-development-with-...

Basics of AUnits:
Test classes can be either local or global. Unit tests should typically be implemented as local classes within the program object that is being tested. However, you can use global test classes to host reusable logic for unit tests across different programs. In Eclipse ADT, there is a separate environment for managing local test classes.

Nihal__Raj_0-1758281914495.png

Reference linkhttps://learning.sap.com/learning-journeys/acquire-core-abap-skills/implementing-code-tests-with-aba...

Structure of test class:

Nihal__Raj_0-1758698995368.png

*"* use this source file for your ABAP unit test classes
class ltc_zi_lfa1_2 DEFINITION FINAL for
  TESTING RISK LEVEL HARMLESS DURATION SHORT.
  PRIVATE SECTION.
  CLASS-DATA environment TYPE REF TO if_cds_test_environment.
  DATA td_zlfa TYPE STANDARD TABLE OF zlfa1 with empty key.
  DATA  act_results type STANDARD TABLE OF zi_lfa1_2 WITH EMPTY KEY.
  CLASS-METHODS class_setup RAISING cx_static_check.
  CLASS-METHODS class_teardown.
  METHODS setup RAISING cx_static_check.
  METHODS prepare_testdata.
  METHODS aunit_for_cds_method FOR TESTING RAISING cx_static_check.
  ENDCLASS.
  

For Testing: The addition FOR TESTING is used to identify a test class. These are the classes that will be executed when you run ABAPUnit tests. I will explain further how AUnits are triggered and how to execute them.

Risk Level: 

  • CRITICAL: The test changes system settings or customizing data.
  • DANGEROUS: The test changes persistent data.
  • HARMLESS: The test does not change system settings or persistent data.

Duration:

  • SHORT: Execution time is imperceptible, expected to take only a few seconds.
  • MEDIUM: Execution time is noticeable, around a minute.
  • LONG: Execution time is very noticeable, taking more than a minute. 

Methods in Test Classes:

  • Helper Methods: Helper methods are ordinary methods of the test class. They are not called by the ABAP Unit framework. You can use helper methods to structure the code of your test methods. For example, prepare_testdata is a helper method.
  • Test Methods: Test methods are defined with the addition FORTESTING after the method name. Each test method represents one test. The ABAP Unit framework performs this test by calling the corresponding test method. Test methods must not have any parameters. In the above example, aunit_for_cds_method is your test method.

Good Practices:

  • It’s a good practice to have a separate method for each functionality, with no dependencies inside the method. Each method should be treated as a single test case.
  • Always include both positive and negative test cases.

Flow of test case:

Nihal__Raj_2-1758283483555.png

CLASS_SETUP
This static method is executed once before the first test of the test class. Use this method for fixtures that are time-consuming to create and for which you are sure the settings will not be changed by any of the test methods. Example: Setting up a test double in the test environment.

SETUP
This instance method is called before each test of the test class. Use this method for fixtures that you want to create fresh for every test case. Example: Creating an instance of the CUT (Class Under Test).

TEARDOWN
This instance method is called after each test of the test class. Use it to undo changes made in the SETUP method. It is especially important if SETUP modifies persistent data (such as system configuration, customizing, or master data). Example: Clearing test doubles so that each test starts with fresh data.

CLASS_TEARDOWN
This static method is executed once after the last test of the test class. Use it to clean up or destroy the test environment set up in CLASS_SETUP, once all tests are completed. Example: Tearing down the overall test environment.

To run ABAP units execute ABAP Unit Test. This will trigger the test class. If you run ABAP application it will not trigger test class.

Test double framework:
As I mentioned earlier, your unit tests should check only the functionality(CUT – Code Under Test) that you have written the test case for. However, in real-world scenarios, each component is usually interlinked and not standalone.

What is the issue?
To explain this, let’s take an example. Suppose you are testing an instance authorization method. This method is dependent on an authorization object. If the AUTH-CHECK of the authorization object fails, the instance authorization method will also fail. This means that even if your instance authorization method is working correctly, the test could still fail due to its dependency. Ideally, the failure of a dependency should not cause your unit test to fail when you are testing the functionality of the CUT itself.

Examples of dependencies in RAP

  • Instance and global authorizations: Dependency on authorization checks.
  • SAVE / SAVE_MODIFIED methods: Dependency on function modules or BAPIs.
  • Determinations: You might use EML of another BO, making your BO dependent.
  • Validations: Your CDS view could be a dependency; if the required record is not present in the CDS view, the validation might fail. And there are many more such cases.

Solution — Test Double Framework
So, how do we overcome this? Instead of relying on dependencies that are beyond our control, we mock or stub these dependencies. This allows us to influence how they behave and interact during testing, thereby avoiding test case failures due to external dependencies. This approach is implemented using the Test Double Framework.

Nihal__Raj_4-1758284209300.png

The main purpose of both mock and stub is to not to test from actual data but rather a fake data that we can configure. Because our test class will fail if its dependent on real time data as it will be different in different
environment.

  1. STUB: Stubs just provide predefined response.
  2. MOCK: You design how the interaction should be. Like if you provide this value this is how the output would be and also verify it.

I will now discuss in detail how we write ABAP Unit tests with a simple example.

Requirement:
Write a positive test case to check the validation check_country, which should throw an exception if we pass a country code with more than2characters. Also, remove the dependency on the CDS view.

Steps:
Identify CUT and dependencies
Here, I have taken a validation method as an example for testing. The CUT(Code Under Test) is the validation method, and the dependency is the CDS view. So, I will use the STUB methodology to configure the response of the CDS view.

Creation of test class
Once you create the test class, you need to add it as a friend in the local handler class (since we are testing a validation). This is necessary because all the methods of the local handler class are inside the private section and cannot be accessed directly.

Nihal__Raj_0-1758702188232.png

Inside the test class
I have already explained the use of SETUP, CLASS_SETUP, TEARDOWN, andCLASS_TEARDOWN earlier, so I will not repeat that here.
In this example, validate_check_country is our test method. I have added a test double for the CDS view, because the validation should be tested using the mock data we provide, not the actual data.
I create the mock data and insert it, so that the CDS view returns this data. Now, when we pass parameters to the validation, we can check if we get a value in reported, since we have provided a country code with 3 characters.

Note: This is just an example program. Typically, mocking, adding mock data, and different functionalities would be implemented in separate methods. It is not a good practice to combine everything into a single method. Each method should have only one responsibility. Here, I have combined them solely to showcase the concept.

Nihal__Raj_0-1758701394247.png

class ltc_lfa1_validation IMPLEMENTATION.
    METHOD class_setup.
      cds_environment = cl_cds_test_environment=>create( i_for_entity = 'zi_lfa1_2' ).
     ENDMETHOD.
     METHOD setup.
       CREATE object cut for testing.
       cds_encironment->clear_doubles( ).
       ENDMETHOD.
     method validate_check_country.
       data: lfa1_mock_data type STANDARD TABLE OF zlfa1.
       data(system_uuid) = cl_uuid_factory=>create_system_uuid( ).
       DATA(uuid_x16) = system_uuid->create_uuid_x16( ).
       select * FROM zi_lfa1_2 into TABLE @DATA(lt_result).
         lfa1_ock_data = VALUE#( ( 
                                  Lifnr = uuid_x16 Name1 = 'Raj' Land1 = 'Ind' ) )
       cds_environment->insert_test_data( i_data = lfa1_mock_data ).
         select * from zi_lfa1_2 into TABLE @DATA(lt_result).
           i_keys = value #( ( Lifnr = uuid_x16 ) ).
           cut-> check_country( exporting key = i_keys
                                changing failed = c_failed reported = c_reported ).
           cl_abap_unit_assert=>assert_not_initial( act = c_reported msg = 'failed' ).
       ENDMETHOD.
       method teardown.
         cds_environment->destroy( ).
         ENDMETHOD.
         METHOD class_teardown.
           cds_environment->clear_doubles( ).
           ENDMETHOD.
    ENDCLASS.

Conclusion: Writing ABAP unit test in RAP ensures that the underling business logic is robust and reliable.  Structured unit test not only improve code quality and reliability but also facilitate easier maintenance and refactoring.

 

 

 

1 Comment
Sandra_Rossi
Active Contributor

Sorry to be a little bit picky: I know very well ABAP Unit and I was interested because you were arguing that your blog post would focus on RAP (title "ABAP Unit for RAP"). But in fact you say that the mocks/test doubles avoid RAP, so I don't learn anything here. The mocks you present are used in classic ABAP Unit. It's just another blog post about ABAP Unit, the other ones contain everything you have presented if I'm not wrong.

EDIT: for information, here are official SAP links which directly concern ABAP Unit and RAP. I show the RAP statements.

SAP TUTORIALS

Write an ABAP Unit Test for the RAP Business Object | SAP Tutorials.

METHOD create_with_action.
   " create a complete composition: Travel (root)
   MODIFY ENTITIES OF ZRAP100_R_TravelTP_###
    ENTITY Travel
    CREATE FIELDS ( AgencyID CustomerID BeginDate EndDate Description TotalPrice BookingFee CurrencyCode )
      WITH VALUE #( (  %cid = 'ROOT1'
                       AgencyID      = agency_mock_data[ 1 ]-agency_id
                       CustomerID    = customer_mock_data[ 1 ]-customer_id
                       BeginDate     = begin_date
                       EndDate       = end_date
                       Description   = 'TestTravel 1'
                       TotalPrice    = '1100'
                       BookingFee    = '20'
                       CurrencyCode  = 'EUR'
                    ) )

    " execute action `acceptTravel`
    ENTITY Travel
      EXECUTE acceptTravel
        FROM VALUE #( ( %cid_ref = 'ROOT1' ) )

   " execute action `deductDiscount`
    ENTITY Travel
      EXECUTE deductDiscount
        FROM VALUE #( ( %cid_ref = 'ROOT1'
                        %param-discount_percent = '20' ) )   "=> 20%

    " result parameters
    MAPPED   DATA(mapped)
    FAILED   DATA(failed)
    REPORTED DATA(reported).

   " expect no failures and messages
   cl_abap_unit_assert=>assert_initial( msg = 'failed'   act = failed ).
   cl_abap_unit_assert=>assert_initial( msg = 'reported' act = reported ).

   " expect a newly created record in mapped tables
   cl_abap_unit_assert=>assert_not_initial( msg = 'mapped-travel'  act = mapped-travel ).

   " persist changes into the database (using the test doubles)
   COMMIT ENTITIES RESPONSES
     FAILED   DATA(commit_failed)
     REPORTED DATA(commit_reported).

   " no failures expected
   cl_abap_unit_assert=>assert_initial( msg = 'commit_failed'   act = commit_failed ).
   cl_abap_unit_assert=>assert_initial( msg = 'commit_reported' act = commit_reported ).

   " read the data from the persisted travel entity (using the test doubles)
   SELECT * FROM ZRAP100_R_TravelTP_### INTO TABLE @DATA(lt_travel). "#EC CI_NOWHERE         
   " assert the existence of the persisted travel entity      
   cl_abap_unit_assert=>assert_not_initial( msg = 'travel from db' act = lt_travel ).
   " assert the generation of a travel ID (key) at creation
   cl_abap_unit_assert=>assert_not_initial( msg = 'travel-id' act = lt_travel[ 1 ]-TravelID ).
   " assert that the action has changed the overall status
   cl_abap_unit_assert=>assert_equals( msg = 'overall status' exp = 'A' act = lt_travel[ 1 ]-OverallStatus ).
   " assert the discounted booking_fee
   cl_abap_unit_assert=>assert_equals( msg = 'discounted booking_fee' exp = '16' act = lt_travel[ 1 ]-BookingFee ).

ENDMETHOD.

SAP HELP - ABAP UNIT FOR BEHAVIOR IMPLEMENTATIONS

ROLLBACK ENTITIES. " in the teardown method
...
DATA failed TYPE RESPONSE FOR FAILED LATE /DMO/I_TRAVEL_M.
DATA reported TYPE RESPONSE FOR REPORTED LATE  /DMO/I_TRAVEL_M.
...
DATA result TYPE TABLE FOR INSTANCE FEATURES RESULT /DMO/I_TRAVEL_M\\travel.
...
DATA mapped   TYPE RESPONSE FOR MAPPED EARLY /DMO/I_TRAVEL_M.
...
READ ENTITY /DMO/I_TRAVEL_M ...
DATA reported TYPE RESPONSE FOR REPORTED LATE /DMO/I_Travel_D.
...
READ ENTITY /DMO/I_Travel_D ...
    DATA(env_config) = cl_botd_txbufdbl_bo_test_env=>prepare_environment_config(
                         )->set_bdef_dependencies( VALUE #( ( cv_i_bdef_name ) )
                         )->handle_draft( VALUE #( ( cv_i_bdef_name ) ) ).

    environment = cl_botd_txbufdbl_bo_test_env=>create( env_config ).
...
    CLASS-DATA:
      environment TYPE REF TO if_botd_txbufdbl_bo_test_env.
    DATA:
      double_supplement TYPE REF TO if_botd_txbufdbl_test_double.
...
    double_supplement =  environment->get_test_double( cv_i_bdef_name ).
    double_supplement->configure_additional_behavior(  )->set_fields_handler( fields_handler = NEW ltd_fields_handler( ) ).
...
  METHOD if_botd_bufdbl_fields_handler~set_readonly_fields.
    CASE entity_name.
      WHEN '/DMO/I_SUPPLEMENT'.
        CASE operation.
          WHEN if_abap_behv=>op-m-create.
            TYPES: ty_create_instances TYPE TABLE FOR CREATE /dmo/i_supplement.
            FIELD-SYMBOLS: <create_instances> TYPE ty_create_instances.
            ASSIGN instances TO <create_instances>.
            LOOP AT <create_instances> ASSIGNING FIELD-SYMBOL(<instance>).
              max_supplement_id += 1.
              <instance>-supplementid = max_supplement_id.
            ENDLOOP.
        ENDCASE.
    ENDCASE.
...
    DATA:
      c_create              TYPE STRUCTURE FOR CREATE /dmo/c_supplement\\supplement,
      c_update              TYPE STRUCTURE FOR UPDATE /dmo/c_supplement\\supplement,
...
    MODIFY ENTITIES OF /dmo/c_supplement
      ENTITY supplement
        CREATE
          FIELDS ( supplementcategory supplementdescription price currencycode )
          WITH VALUE #( ( c_create ) )
        UPDATE
          FIELDS ( supplementdescription price currencycode )
          WITH VALUE #( ( c_update ) )
...
...
CLASS lsc_r_agency DEFINITION INHERITING FROM cl_abap_behavior_saver FRIENDS ltcl_sc_r_agency.
...
    event_test_environment = cl_rap_event_test_environment=>create( VALUE #( ( entity_name = agency_entity event_name = agency_event ) ) ).
...
    event_test_environment->get_event( entity_name = agency_entity event_name = agency_event )->verify( )->is_raised_times( 0 ).
...
    DATA event_payload_act TYPE TABLE FOR EVENT /dmo/i_agencytp~/dmo/agencyreviewcreated.
    event_payload_act = event_test_environment->get_event( entity_name  = agency_entity event_name = agency_event )->get_payload( )->*.

SAP HELP - ABAP UNIT FOR EML INTEGRATION TESTS

EML Integration Tests | SAP Help Portal

    MODIFY ENTITIES OF /dmo/i_travel_m
      ENTITY travel
        CREATE FIELDS ( agency_id customer_id begin_date end_date description booking_fee currency_code overall_status ) WITH
...
    " Trigger Validations
    COMMIT ENTITIES
    RESPONSE OF /DMO/I_Travel_M
    FAILED DATA(failed_commit)
    REPORTED DATA(reported_commit).
...
    cl_abap_unit_assert=>assert_equals( exp = 004 act = reported_commit-travel[ 1 ]-%msg->if_t100_message~t100key-msgno ).
...
    MODIFY ENTITIES OF /dmo/i_travel_m
      ENTITY travel
        CREATE FIELDS (    agency_id customer_id begin_date end_date description booking_fee currency_code overall_status ) WITH
        VALUE #( (         %cid = cid
...
        CREATE BY \_booking FIELDS ( booking_date customer_id carrier_id connection_id flight_date flight_price currency_code 
                                     booking_status ) 
        WITH VALUE #( ( %cid_ref = cid
...
    MODIFY ENTITIES OF /dmo/i_travel_m
     ENTITY travel
       EXECUTE acceptTravel FROM VALUE #( ( travel_id = mapped_create-travel[ 1 ]-%tky ) )
...
    MODIFY ENTITIES OF /dmo/i_travel_m
    ENTITY travel
      CREATE BY \_booking FIELDS ( booking_date customer_id carrier_id connection_id flight_date flight_price currency_code booking_status ) WITH
...

SAP HELP - ABAP UNIT FOR ODATA INTEGRATION TESTS

OData Integration Tests | SAP Help Portal

METHOD class_setup.
    go_client_proxy = create_local_client_proxy( VALUE #(
                                      service_id      = '/DMO/UI_BOOKING_O2'
                                      service_version = '0001' )  ).
ENDMETHOD.
...
METHOD create_local_client_proxy.
    TRY.
        " the Cloud version
        DATA(class1) = 'CL_WEB_ODATA_CLIENT_FACTORY'.
        CALL METHOD (class1)=>create_v2_local_proxy
          EXPORTING
            is_service_key  = service_key
          RECEIVING
            ro_client_proxy = client_proxy.
      CATCH cx_root.  
    ENDTRY.
    IF client_proxy IS NOT BOUND.
      TRY.
          " the onPrem version
          DATA(class2) = '/IWBEP/CL_CP_CLIENT_PROXY_FACT'.
          CALL METHOD (class2)=>create_v2_local_proxy
            EXPORTING
              is_service_key  = service_key
            RECEIVING
              ro_client_proxy = client_proxy.
        CATCH cx_root.  
      ENDTRY.
    ENDIF.
ENDMETHOD.
...
    DATA(lo_request) = go_client_proxy->create_resource_for_entity_set( 'Booking' )->create_request_for_read(  ).
    DATA(lo_response) = lo_request->execute( ).
    cl_abap_unit_assert=>assert_not_initial( lo_response ).
    DATA ls_response_data TYPE STANDARD TABLE OF /DMO/C_Booking_VE.
    lo_response->get_business_data( IMPORTING et_business_data = ls_response_data ).
...
    lo_filter_factory = lo_request->create_filter_factory( ).
    lt_range_currencycode = VALUE #( ( sign = 'I' option = 'EQ' low = 'EUR' ) ).
    lo_filter_node  = lo_filter_factory->create_by_range( iv_property_path     = 'CURRENCYCODE'
                                                          it_range             =  lt_range_currencycode ).
...
    lo_request->set_filter( lo_filter_node ).
    lo_request->set_top( iv_top = 2 ).
    DATA(lo_response) = lo_request->execute( ).
...