RAP - ABAP Unit (Business Object)
At this point, we'll take a look at how we can test our RAP Business Object automatically in order to check all important functions at the touch of a button.
Table of contents
In an older series, we already looked at ABAP Unit and how you can use it to test your code automatically. In this article we look at how you can get your RAP object validated and what you can check in the most useful way.
ABAP Unit
This term includes all methods for automatically testing ABAP code in the system, and there are various techniques for making the code testable. On our overview page you will find everything about ABAP Unit and how you can use it, in this article we assume the knowledge as a basis.
Write test
We now create a global test class for the test cases. While we could create a local test class, we only want to test the public interfaces of our RAP object and separate the test from the original object. To do this, we create the following test class as a wrapper for the tests:
"! @testing ZBS_I_RAPPartner
CLASS zcl_bs_demo_unit_rap DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS:
create_new_entry FOR TESTING,
fill_empty_streets FOR TESTING,
clear_empty_streets FOR TESTING.
ENDCLASS.
CLASS zcl_bs_demo_unit_rap IMPLEMENTATION.
METHOD create_new_entry.
ENDMETHOD.
METHOD fill_empty_streets.
ENDMETHOD.
METHOD clear_empty_streets.
ENDMETHOD.
ENDCLASS.
We create the test class as a global class. The corresponding additions (FOR TESTING, DURATION, RISK LEVEL) must also be specified. We link the test class to the Core Data Service via the ABAP Doc comment. To do this, we want to implement three tests:
- Creating a new entry and checking the generated key
- Filling an entry with an empty street
- Delete all entries with an empty street
New entry
In the first step, the test fills a table with new data and marks the errors as relevant, then we insert the new data records and commit the data to the database. Then we check the return for errors. Finally, we read from the database whether the new entry was actually created.
DATA:
lt_new_partner TYPE TABLE FOR CREATE ZBS_I_RAPPartner.
lt_new_partner = VALUE #(
( partnername = 'Do it Yourself'
street = 'Waterloo Street 13'
city = 'London'
country = 'GB'
paymentcurrency = 'GBP'
%control-PartnerName = if_abap_behv=>mk-on
%control-Street = if_abap_behv=>mk-on
%control-City = if_abap_behv=>mk-on
%control-Country = if_abap_behv=>mk-on
%control-PaymentCurrency = if_abap_behv=>mk-on
)
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner CREATE FROM lt_new_partner
MAPPED DATA(ls_mapped).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
cl_abap_unit_assert=>assert_initial( ls_commit_reported-partner ).
cl_abap_unit_assert=>assert_initial( ls_commit_failed-partner ).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, name
WHERE name = 'Do it Yourself'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
Fill empty street
In the first step we transfer a partner for whom we want to carry out the action, we confirm the action with a commit and then read the current status from the database. The street should now read "EMPTY" and the entry should be found by the database.
DATA:
lt_fill_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~fillEmptyStreets.
lt_fill_streets = VALUE #(
( PartnerNumber = '2000000001' )
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE fillEmptyStreets FROM lt_fill_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, Street
WHERE partner = '2000000001'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
cl_abap_unit_assert=>assert_equals( act = ls_partner_found-street exp = 'EMPTY' ).
Delete EMPTY entries
For the execution of the static action we have to fill the table with an empty entry so that the logic is executed at all. Then the action is executed and confirmed with a commit. Finally, we read the database again to see if there are any records with EMPTY.
DATA:
lt_clear_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~clearAllEmptyStreets.
INSERT INITIAL LINE INTO TABLE lt_clear_streets.
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE clearAllEmptyStreets FROM lt_clear_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT FROM zbs_dmo_partner
FIELDS partner
WHERE street = 'EMPTY'
INTO TABLE @DATA(lt_empty_streets).
cl_abap_unit_assert=>assert_subrc( exp = 4 ).
After running the unit test we now get a result, but will this test always be consistent and can we always expect the same data for the test? In the next section we will tackle this problem.
Mock data
The first test cases have now been created, but run against the real data in the database. If this data changes or records are deleted, our test cases will no longer work. We now want to remove these dependencies by replacing the data in the database with new test data at the time of the test, so that we always have the right test data for our test cases. To do this, we work with a test double for the underlying Core Data Service, which means we need a CDS double.
In the first step we expand the PRIVATE SECTION and create a variable to hold the double. Furthermore, we implement the methods CLASS_SETUP and CLASS_TEARDOWN to prepare the data for the test:
PRIVATE SECTION.
CLASS-DATA:
go_environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS:
class_setup RAISING cx_static_check,
class_teardown.
Then we create the double for the interface view on the reference of the table and activate the redirection to the test double in the framework. The teardown method is only implemented in order to break down the test double again at the end.
METHOD class_setup.
DATA:
lt_partner TYPE STANDARD TABLE OF zbs_dmo_partner WITH EMPTY KEY.
go_environment = cl_cds_test_environment=>create(
i_for_entity = 'ZBS_I_RAPPARTNER'
i_dependency_list = VALUE #( ( name = 'ZBS_DMO_PARTNER' type ='TABLE' ) )
).
lt_partner = VALUE #(
( partner = '2000000001' name = 'Las Vegas Corp' country = 'US' payment_currency = 'USD' )
( partner = '2000000002' name = 'Gorillas' street = 'Main street 10' country = 'DE' payment_currency = 'EUR' )
( partner = '2000000003' name = 'Tomato Inc' street = 'EMPTY' country = 'AU' payment_currency = 'AUD' )
).
go_environment->insert_test_data( lt_partner ).
go_environment->enable_double_redirection( ).
ENDMETHOD.
METHOD class_teardown.
go_environment->destroy( ).
ENDMETHOD.
For our test, we implement a different "number range" to separate the data from the original data. Furthermore, we only add some test data to keep the test clear.
Complete example
Here is the entire test class again with all test methods, as always you can also find them in the series' Git repository:
"! @testing ZBS_I_RAPPartner
CLASS zcl_bs_demo_unit_rap DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PUBLIC SECTION.
PROTECTED SECTION.
PRIVATE SECTION.
CLASS-DATA:
go_environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS:
class_setup RAISING cx_static_check,
class_teardown.
METHODS:
create_new_entry FOR TESTING,
fill_empty_streets FOR TESTING,
clear_empty_streets FOR TESTING.
ENDCLASS.
CLASS zcl_bs_demo_unit_rap IMPLEMENTATION.
METHOD class_setup.
DATA:
lt_partner TYPE STANDARD TABLE OF zbs_dmo_partner WITH EMPTY KEY.
go_environment = cl_cds_test_environment=>create(
i_for_entity = 'ZBS_I_RAPPARTNER'
i_dependency_list = VALUE #( ( name = 'ZBS_DMO_PARTNER' type ='TABLE' ) )
).
lt_partner = VALUE #(
( partner = '2000000001' name = 'Las Vegas Corp' country = 'US' payment_currency = 'USD' )
( partner = '2000000002' name = 'Gorillas' street = 'Main street 10' country = 'DE' payment_currency = 'EUR' )
( partner = '2000000003' name = 'Tomato Inc' street = 'EMPTY' country = 'AU' payment_currency = 'AUD' )
).
go_environment->insert_test_data( lt_partner ).
go_environment->enable_double_redirection( ).
ENDMETHOD.
METHOD class_teardown.
go_environment->destroy( ).
ENDMETHOD.
METHOD create_new_entry.
DATA:
lt_new_partner TYPE TABLE FOR CREATE ZBS_I_RAPPartner.
lt_new_partner = VALUE #(
( partnername = 'Do it Yourself'
street = 'Waterloo Street 13'
city = 'London'
country = 'GB'
paymentcurrency = 'GBP'
%control-PartnerName = if_abap_behv=>mk-on
%control-Street = if_abap_behv=>mk-on
%control-City = if_abap_behv=>mk-on
%control-Country = if_abap_behv=>mk-on
%control-PaymentCurrency = if_abap_behv=>mk-on
)
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner CREATE FROM lt_new_partner
MAPPED DATA(ls_mapped).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
cl_abap_unit_assert=>assert_initial( ls_commit_reported-partner ).
cl_abap_unit_assert=>assert_initial( ls_commit_failed-partner ).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, name
WHERE name = 'Do it Yourself'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
ENDMETHOD.
METHOD fill_empty_streets.
DATA:
lt_fill_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~fillEmptyStreets.
lt_fill_streets = VALUE #(
( PartnerNumber = '2000000001' )
).
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE fillEmptyStreets FROM lt_fill_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT SINGLE FROM zbs_dmo_partner
FIELDS partner, Street
WHERE partner = '2000000001'
INTO @DATA(ls_partner_found).
cl_abap_unit_assert=>assert_subrc( ).
cl_abap_unit_assert=>assert_equals( act = ls_partner_found-street exp = 'EMPTY' ).
ENDMETHOD.
METHOD clear_empty_streets.
DATA:
lt_clear_streets TYPE TABLE FOR ACTION IMPORT ZBS_I_RAPPartnerPartner~clearAllEmptyStreets.
INSERT INITIAL LINE INTO TABLE lt_clear_streets.
MODIFY ENTITIES OF ZBS_I_RAPPartner
ENTITY Partner EXECUTE clearAllEmptyStreets FROM lt_clear_streets
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
COMMIT ENTITIES
RESPONSE OF ZBS_I_RAPPartner
REPORTED DATA(ls_commit_reported)
FAILED DATA(ls_commit_failed).
SELECT FROM zbs_dmo_partner
FIELDS partner
WHERE street = 'EMPTY'
INTO TABLE @DATA(lt_empty_streets).
cl_abap_unit_assert=>assert_subrc( exp = 4 ).
ENDMETHOD.
ENDCLASS.
Conclusion
Implementing a unit test for a RAP business object is not difficult, but only by mocking the data from the database can stable tests be built. In the end, these tests also ensure that the object is used sensibly when it is extended.