Application Development 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: 
satish_inamdar
Explorer
2,317

Foreign ABAP Unit Tests


Yes, we are all familiar with ABAP Unit Tests and the principles of test driven development. If not, there are a lot of blogs and literature around ABAP Unit Test Framework. I will link a few at the end of this blog as well in case you require some pre-requisite reading.

In this blog, I will specifically explore the concept of Foreign ABAP Unit Tests and where they can be used.

Foreign ABAP Unit Tests can be created using the ABAP Unit Test Framework and run using the ABAP Unit Test Runner.

Foreign ABAP Unit Tests can be created in ADT using the ABAP Doc comments:
" ! @testing

This ABAP Doc comment links the test class or test method with the repository object specified after @testing

Let's see the same in action first with an example using an ABAP Class to make the concept clearer, before we dive into scenarios where writing such foreign unit tests can be useful.

Let's consider an ABAP Class ZCL_FUEL_INFO that has one method CALC_FUEL_PRICE which takes in three importing parameters: VEHICLE_TYPE, DISTANCE and FUEL_PRICE. Based on these three input parameters the method returns the fuel price.
CLASS zcl_vehicle_info DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.

PUBLIC SECTION.
TYPES: BEGIN OF ENUM vehicle_make,
car,
bike,
truck,
END OF ENUM vehicle_make.
TYPES: float TYPE p LENGTH 10 DECIMALS 2.
METHODS calc_fuel_price
IMPORTING
vehicle_type TYPE vehicle_make
fuel_rate TYPE float
distance TYPE float
RETURNING
VALUE(fuel_price) TYPE float.

PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.

CLASS zcl_vehicle_info IMPLEMENTATION.
METHOD calc_fuel_price.
IF fuel_rate <= 0 or distance <= 0.
fuel_price = 0.
EXIT.
ENDIF.
CASE vehicle_type.
WHEN car.
fuel_price = ( distance * fuel_rate ) / 20.
WHEN bike.
fuel_price = ( distance * fuel_rate ) / 40.
WHEN truck.
fuel_price = ( distance * fuel_rate ) / 5.
ENDCASE.
ENDMETHOD.
ENDCLASS.

Here, instead of creating a local ABAP unit test class for the above class, lets create a new global class as shown below in the code:
"! @testing zcl_vehicle_info
CLASS zcl_vehicle_info_test DEFINITION
FOR TESTING
FINAL
DURATION SHORT
RISK LEVEL HARMLESS
PUBLIC.

PRIVATE SECTION.
METHODS setup.
METHODS test_fuel_price_bike FOR TESTING.
METHODS test_fuel_price_car FOR TESTING.
METHODS test_fuel_price_truck FOR TESTING.
METHODS test_fuel_price_invalid FOR TESTING.

DATA: vehicle_ref TYPE REF TO zcl_vehicle_info.
ENDCLASS.

CLASS zcl_vehicle_info_test IMPLEMENTATION.
METHOD setup.
vehicle_ref = NEW #( ).
ENDMETHOD.

METHOD test_fuel_price_bike.
cl_abap_unit_assert=>assert_equals(
act = vehicle_ref->calc_fuel_price( vehicle_type = vehicle_ref->bike
distance = 200
fuel_rate = 95 )
exp = 475
msg = `Wrong Fuel Price`
quit = cl_aunit_assert=>no ).
ENDMETHOD.

METHOD test_fuel_price_car.
cl_abap_unit_assert=>assert_equals(
act = vehicle_ref->calc_fuel_price( vehicle_type = vehicle_ref->car
distance = 100
fuel_rate = 95 )
exp = 475
msg = `Wrong Fuel Price`
quit = cl_aunit_assert=>no ).
ENDMETHOD.

METHOD test_fuel_price_truck.
cl_abap_unit_assert=>assert_equals(
act = vehicle_ref->calc_fuel_price( vehicle_type = vehicle_ref->truck
distance = 100
fuel_rate = 85 )
exp = 1700
msg = `Wrong Fuel Price`
quit = cl_aunit_assert=>no ).
ENDMETHOD.

METHOD test_fuel_price_invalid.
cl_abap_unit_assert=>assert_equals(
act = vehicle_ref->calc_fuel_price( vehicle_type = vehicle_ref->bike
distance = -10
fuel_rate = 95 )
exp = 0
msg = `Wrong Fuel Price`
quit = cl_aunit_assert=>no ).
ENDMETHOD.
ENDCLASS.

 

To execute the Foreign Unit Test Class, In ADT go to original class (the class which has to be tested, in our case: ZCL_VEHICLE_INFO) and select from application menu bar: Run As -> ABAP Unit Tests with as shown in the screenshot below:


Choosing the relevant Run As option in ADT


 


Choose Foreign Tests Option in the Pop-up


 


Foreign Unit Test Results


If you would have selected "Coverage" as an additional measurement in the previous pop-up then code coverage by the foreign unit test class is also shown


Foreign Unit Test Results with Code Coverage



When will it be useful?


In the above example whatever could be achieved via Foreign Unit Test Class could also be have achieved using a Local Unit Test Class and that is a fair argument. Now, lets look at a scenario where we have a CDS View. A CDS View doesn't have a provision to create and associate with it a local test class, so how can we then create and associate a test class for it? Well, in the case we go for the Foreign Unit Test Class.

Let's see the same with an example.

Here is a simplistic CDS View, that has some columns which are based on calculations:
@AbapCatalog.sqlViewName: 'ZDVSMEALD'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Calculate Meal Discount'
define view ZCDS_SMEAL_CALC_DISC
as select from smeal
{
key carrid as Carrid,
key mealnumber as Mealnumber,
mealtype as Mealtype,
case mealtype
when 'VE' then 10
when 'FI' then 20
when 'FL' then 15
else 5
end as disc_percent

}

For this CDS View, we can create a test unit class using CDS test double framework and link it to this CDS view using the annotation "! @testing as shown below: 

Create the following global class:
"! @testing ZCDS_SMEAL_CALC_DISC
CLASS zcl_test_meal_disc DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS PUBLIC.


PRIVATE SECTION.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS test_veg_disc FOR TESTING.
METHODS test_fish_disc FOR TESTING.
METHODS test_nonveg_disc FOR TESTING.
METHODS test_unknown_disc FOR TESTING.
CLASS-DATA:
environment TYPE REF TO if_cds_test_environment.
ENDCLASS.

CLASS zcl_test_meal_disc IMPLEMENTATION.

METHOD class_setup.
environment = cl_cds_test_environment=>create( i_for_entity = 'ZCDS_SMEAL_CALC_DISC' ).
ENDMETHOD.

METHOD class_teardown.
environment->destroy( ).
ENDMETHOD.

METHOD setup.
environment->clear_doubles( ).
ENDMETHOD.

METHOD test_veg_disc.

DATA: smeal TYPE TABLE OF smeal,
act_results TYPE TABLE OF zcds_smeal_calc_disc,
exp_results TYPE TABLE OF zcds_smeal_calc_disc,
test_data TYPE REF TO if_cds_test_data.

smeal = VALUE #( ( mandant = sy-mandt carrid = 'AA' mealnumber = 10 mealtype = 'VE' ) ).
test_data = cl_cds_test_data=>create( i_data = smeal ).
"Create test double
DATA(smeal_double) = environment->get_double( i_name = 'SMEAL' ).
"Insert data into test double
smeal_double->insert( test_data ).
"Fetch data from test double
SELECT * FROM zcds_smeal_calc_disc INTO TABLE @act_results.
"Prepare expected output
exp_results = VALUE #( ( carrid = 'AA' mealnumber = 10 mealtype = 'VE' disc_percent = 10 ) ).

cl_abap_unit_assert=>assert_equals(
act = act_results[ 1 ]-disc_percent
exp = exp_results[ 1 ]-disc_percent ).
ENDMETHOD.

METHOD test_fish_disc.

DATA: smeal TYPE TABLE OF smeal,
act_results TYPE TABLE OF zcds_smeal_calc_disc,
exp_results TYPE TABLE OF zcds_smeal_calc_disc,
test_data TYPE REF TO if_cds_test_data.

smeal = VALUE #( ( mandant = sy-mandt carrid = 'AA' mealnumber = 10 mealtype = 'FI' ) ).
test_data = cl_cds_test_data=>create( i_data = smeal ).
DATA(smeal_double) = environment->get_double( i_name = 'SMEAL' ).
smeal_double->insert( test_data ).

SELECT * FROM zcds_smeal_calc_disc INTO TABLE @act_results.

exp_results = VALUE #( ( carrid = 'AA' mealnumber = 10 mealtype = 'FI' disc_percent = 20 ) ).

cl_abap_unit_assert=>assert_equals(
act = act_results[ 1 ]-disc_percent
exp = exp_results[ 1 ]-disc_percent ).
ENDMETHOD.

METHOD test_nonveg_disc.

DATA: smeal TYPE TABLE OF smeal,
act_results TYPE TABLE OF zcds_smeal_calc_disc,
exp_results TYPE TABLE OF zcds_smeal_calc_disc,
test_data TYPE REF TO if_cds_test_data.

smeal = VALUE #( ( mandant = sy-mandt carrid = 'AA' mealnumber = 10 mealtype = 'FL' ) ).
test_data = cl_cds_test_data=>create( i_data = smeal ).
DATA(smeal_double) = environment->get_double( i_name = 'SMEAL' ).
smeal_double->insert( test_data ).

SELECT * FROM zcds_smeal_calc_disc INTO TABLE @act_results.

exp_results = VALUE #( ( carrid = 'AA' mealnumber = 10 mealtype = 'FL' disc_percent = 15 ) ).

cl_abap_unit_assert=>assert_equals(
act = act_results[ 1 ]-disc_percent
exp = exp_results[ 1 ]-disc_percent ).
ENDMETHOD.

METHOD test_unknown_disc.

DATA: smeal TYPE TABLE OF smeal,
act_results TYPE TABLE OF zcds_smeal_calc_disc,
exp_results TYPE TABLE OF zcds_smeal_calc_disc,
test_data TYPE REF TO if_cds_test_data.

smeal = VALUE #( ( mandant = sy-mandt carrid = 'AA' mealnumber = 10 mealtype = 'ZZ' ) ).
test_data = cl_cds_test_data=>create( i_data = smeal ).
DATA(smeal_double) = environment->get_double( i_name = 'SMEAL' ).
smeal_double->insert( test_data ).

SELECT * FROM zcds_smeal_calc_disc INTO TABLE @act_results.

exp_results = VALUE #( ( carrid = 'AA' mealnumber = 10 mealtype = 'ZZ' disc_percent = 7 ) ).

cl_abap_unit_assert=>assert_equals(
act = act_results[ 1 ]-disc_percent
exp = exp_results[ 1 ]-disc_percent ).

ENDMETHOD.
ENDCLASS.

Notice the annotation added in the first line.

Now in your ADT, go to your CDS View and select the following option from the drop down:


ABAP Unit Test Execution for CDS


This opens up a new pop up as shown below, click on OK button to proceed


Pop up to confirm unit test execution


Results of Foreign Unit Tests are displayed in ADT as shown below


CDS Unit Test (Foreign tests) Results


As you can see, one of the unit test has failed, this is intentional as the expected discount percentage when meal type couldn't be determined is 7 but CDS view is currently calculating it as 5.

In conclusion, that is how you can use Foreign Unit Tests. I hope you found this blog helpful and that it has piqued your interest in exploring and learning more about ABAP Unit Tests. Foreign Unit Tests are a valuable tool that can help you to ensure the quality of your code. They can be used to test individual units of code, as well as to test the interactions between different units of code. If you are not already using Foreign Unit Tests, I encourage you to give them a try. I am confident that you will find them to be a valuable addition to your development toolkit.

As mentioned earlier, I am sharing below a links of 5 blogs that I found really helpful:

https://blogs.sap.com/2019/12/20/understanding-abap-unit-testing-fundamentals-overview-for-beginners...

https://blogs.sap.com/2019/12/25/test-doubles-and-using-osql-test-double-framework-for-abap-unit-tes...

This one is a series in its own, so I am just pasting the link of the first one:

https://blogs.sap.com/2021/03/31/getting-acquainted-with-automating-abap-unit-testing-part-1/

https://blogs.sap.com/2018/08/30/cds-doubles-writing-unit-tests-for-abap-cds/

https://blogs.sap.com/2016/10/19/introduction-cds-test-double-framework-write-unit-tests-abap-cds-en...

 

Happy Learning!

 

 
1 Comment
Labels in this area