CDS Doubles – Writing Unit Tests for ABAP CDS
Hello Community,
This blog stems from my recent learning where I had to create CDS doubles to implement automation testing for the delivered ABAP CDS views.
Before going deeper in the topic I would like to thank below colleagues who helped me in realizing the same. Thank you Sunil Bandameedapalli and Krishan Raheja.
The motivation and theory has been very well explained here, so I will start right away with my demo implementation and its variation. If you are new to this topic please read the above blog once to know the terminology.
I will demonstrate below two cases.
- CDS Under Test (CUT) with only tables as its Depended Upon Components(DOC).
- CUT with tables and other CDS views as its DOC.
Further variations and mix scenarios are also possible.
Before starting the implementation steps, I will outline the approach which I followed.
i) CUT identification – It should be non-trivial and have code pushdown or some complex join conditions.
ii) DOCs identification – In this step we breakdown the CUT into its components tables and other CDS views.
iii) Create a global abstract abap unit test class.
iv) Implement the local class within the step iii)’s global class which will do the following
a) Create fixture methods
b) Populate data in the DOCs
c) Execute the CDS view to fetch the data
d) Using Asserts compare the results
e) Test the CDS double
Please note: Above steps are same for both kinds of CDS test double implementation. Actually, case 2 is a minor variation of case 1.
In Detail –
Case 1.CDS Under Test (CUT) with only tables as its Depended Upon Components(DOC).
i) CUT identification –
This CDS derives data from multiple joins and hence becomes a candidate for automation testing.
@AbapCatalog.sqlViewName: 'IASORDDFCSA'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales order, Sales order document flow, customer sales data'
define view I_ASALESORDDOCFLWCUSTSALE
as select from vbap as sales_item
left outer join vbak as sales_header on sales_item.vbeln = sales_header.vbeln
left outer join knvv as customer_sales on customer_sales.kunnr = sales_header.kunnr
and customer_sales.vkorg = sales_header.vkorg
and customer_sales.vtweg = sales_header.vtweg
and customer_sales.spart = sales_header.spart
left outer join vbfa as sales_doc_flow on sales_item.vbeln = sales_doc_flow.vbelv
and sales_item.posnr = sales_doc_flow.posnv
and sales_doc_flow.vbtyp_n = 'J'
left outer join likp as delivery on sales_doc_flow.vbeln = delivery.vbeln
left outer join ztasalesofc as sales_office on sales_office.sales_ofc = sales_header.kvgr5
or sales_office.sales_ofc = customer_sales.kvgr5
left outer join ztataxofc_t as _Text on sales_office.tax_ofc_num = _Text.tax_ofc_num
and _Text.spras = $session.system_language
left outer join tvv5t as sales_office_text on (
sales_office_text.kvgr5 = sales_header.kvgr5
or sales_office_text.kvgr5 = customer_sales.kvgr5
)
and sales_office_text.spras = $session.system_language
{
sales_header.vbeln as SalesDocument,
sales_item.posnr as SalesDocumentItem,
sales_header.auart as SalesDocumentType,
customer_sales.vkorg as SalesOrganization,
sales_header.kvgr5 as SalesordSalesOffice,
customer_sales.kunnr as Customer,
customer_sales.vtweg as DistributionChannel,
customer_sales.spart as Division,
customer_sales.kvgr1 as CustomerType,
customer_sales.kvgr5 as CustSalesOffice,
sales_doc_flow.vbeln as Delivery,
sales_doc_flow.posnn as DeliveryItem,
delivery.podat as ProofOfDelivery,
sales_office.tax_ofc_num as TaxOffice,
_Text.tax_ofc_name as TaxOfficeName,
_Text.spras as language_tax_office,
sales_office_text.bezei as SalesOfficeText,
sales_office_text.spras as language_sales_office
}
ii) DOCs identification –
As evident from the CDS definition, the involved tables are – VBAP, VBAK, KNVV, VBFA, LIKP, ZTASALESOFC, ZTATAXOFC_T and TVV5T.
iii) The created global class via SE24 is –
iv) Press ( Ctrl+Shift+F11 ) for the above global class and create a local class as below
*"* use this source file for your ABAP unit test classes
************************************************************************
* This CDS Double is used to test the below CDS
* CDS shows Sales order, Sales order document flow, customer sales data
************************************************************************
CLASS i_asalesorddocflwcustsale
DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS: class_setup,
class_teardown.
METHODS: setup,
dis_so_cust_data FOR TESTING RAISING cx_static_check.
DATA: vbap TYPE STANDARD TABLE OF vbap WITH EMPTY KEY,
vbak TYPE STANDARD TABLE OF vbak WITH EMPTY KEY,
knvv TYPE STANDARD TABLE OF knvv WITH EMPTY KEY,
vbfa TYPE STANDARD TABLE OF vbfa WITH EMPTY KEY,
likp TYPE STANDARD TABLE OF likp WITH EMPTY KEY,
tasalesofc TYPE STANDARD TABLE OF ztasalesofc WITH EMPTY KEY,
tataxofc_t TYPE STANDARD TABLE OF ztataxofc_t WITH EMPTY KEY,
tvv5t TYPE STANDARD TABLE OF tvv5t WITH EMPTY KEY.
ENDCLASS.
CLASS i_asalesorddocflwcustsale IMPLEMENTATION.
ENDCLASS.
a) Implement below fixture methods
METHOD class_setup.
" Instantiate the CUT and the inject the dependent on components( DOC )
" This will create the doubles as well for the DOCs
environment = cl_cds_test_environment=>create( i_for_entity = 'I_ASALESORDDOCFLWCUSTSALE'
i_dependency_list = VALUE #(
( name = 'VBAP' type = 'TABLE' )
( name = 'VBAK' type = 'TABLE' )
( name = 'KNVV' type = 'TABLE' )
( name = 'VBFA' type = 'TABLE' )
( name = 'LIKP' type = 'TABLE' )
( name = 'ZTASALESOFC' type = 'TABLE' )
( name = 'ZTATAXOFC_T' type = 'TABLE' )
( name = 'TVV5T' type = 'TABLE' )
) ).
ENDMETHOD.
METHOD class_teardown.
" Iterate over the double list and remove all
environment->destroy( ).
ENDMETHOD.
METHOD setup.
"Clean all the data in the doubles (Depenedent on components)
environment->clear_doubles( ).
ENDMETHOD.
b), c) and d) I have combined all three. You can try implementing these separately as well.
METHOD dis_so_cust_data.
" Populate the data in the double stubs
vbap = VALUE #( ( vbeln = '1000000258' posnr = '000010' ) ).
environment->insert_test_data( i_data = vbap ).
vbak = VALUE #( ( vbeln = '1000000258' kunnr = '0000000104' vkorg = '2010' vtweg = '10' spart ='10' kvgr5 = 'A15' auart = 'ZA01' ) ).
environment->insert_test_data( i_data = vbak ).
knvv = VALUE #( ( kunnr = '0000000104' vkorg = '2010' vtweg = '10' spart ='10' kvgr5 = 'A15' kvgr1 = 'Z01' ) ).
environment->insert_test_data( i_data = knvv ).
vbfa = VALUE #( ( vbelv = '1000000258' posnv = '000010' vbtyp_n = 'J' vbeln = '2000000283' posnn = '000010' ) ).
environment->insert_test_data( i_data = vbfa ).
likp = VALUE #( ( vbeln = '2000000283' podat ='00000000' ) ).
environment->insert_test_data( i_data = likp ).
tasalesofc = VALUE #( ( sales_ofc = 'A15' tax_ofc_num = '01145' ) ).
environment->insert_test_data( i_data = tasalesofc ).
tataxofc_t = VALUE #( ( tax_ofc_num = '01145' spras = 'E' tax_ofc_name = 'Test TaxOf' ) ).
environment->insert_test_data( i_data = tataxofc_t ).
tvv5t = VALUE #( ( kvgr5 = 'A15' spras = 'E' bezei = 'TEST Purpose' ) ).
environment->insert_test_data( i_data = tvv5t ).
" Execute the CUT which in turn will fetch the data from the doubles and
" then validate the output
SELECT * FROM i_asalesorddocflwcustsale
INTO TABLE @DATA(lt_so_cust_data).
cl_abap_unit_assert=>assert_equals( act = lines( lt_so_cust_data )
exp = 1 ).
ENDMETHOD.
e) Testing the double
Open the global class via SE24 and choose Execute Unit tests with coverage option as below –
The execution result will look like below –
Complete code base for case 1.
*"* use this source file for your ABAP unit test classes
************************************************************************
* This CDS Double is used to test the below CDS
* CDS shows Sales order, Sales order document flow, customer sales data
************************************************************************
CLASS i_asalesorddocflwcustsale
DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA: environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS: class_setup,
class_teardown.
METHODS: setup,
dis_so_cust_data FOR TESTING RAISING cx_static_check.
DATA: vbap TYPE STANDARD TABLE OF vbap WITH EMPTY KEY,
vbak TYPE STANDARD TABLE OF vbak WITH EMPTY KEY,
knvv TYPE STANDARD TABLE OF knvv WITH EMPTY KEY,
vbfa TYPE STANDARD TABLE OF vbfa WITH EMPTY KEY,
likp TYPE STANDARD TABLE OF likp WITH EMPTY KEY,
tasalesofc TYPE STANDARD TABLE OF ztasalesofc WITH EMPTY KEY,
tataxofc_t TYPE STANDARD TABLE OF ztataxofc_t WITH EMPTY KEY,
tvv5t TYPE STANDARD TABLE OF tvv5t WITH EMPTY KEY.
ENDCLASS.
CLASS i_asalesorddocflwcustsale IMPLEMENTATION.
METHOD class_setup.
" Instantiate the CUT and the inject the dependent on components( DOC )
" This will create the doubles as well for the DOCs
environment = cl_cds_test_environment=>create( i_for_entity = 'I_ASALESORDDOCFLWCUSTSALE'
i_dependency_list = VALUE #(
( name = 'VBAP' type = 'TABLE' )
( name = 'VBAK' type = 'TABLE' )
( name = 'KNVV' type = 'TABLE' )
( name = 'VBFA' type = 'TABLE' )
( name = 'LIKP' type = 'TABLE' )
( name = 'ZTASALESOFC' type = 'TABLE' )
( name = 'ZTATAXOFC_T' type = 'TABLE' )
( name = 'TVV5T' type = 'TABLE' )
) ).
ENDMETHOD.
METHOD class_teardown.
" Iterate over the double list and remove all
environment->destroy( ).
ENDMETHOD.
METHOD setup.
"Clean all the data in the doubles (Depenedent on components)
environment->clear_doubles( ).
ENDMETHOD.
METHOD dis_so_cust_data.
" Populate the data in the double stubs
vbap = VALUE #( ( vbeln = '1000000258' posnr = '000010' ) ).
environment->insert_test_data( i_data = vbap ).
vbak = VALUE #( ( vbeln = '1000000258' kunnr = '0000000104' vkorg = '2010' vtweg = '10' spart ='10' kvgr5 = 'A15' auart = 'ZA01' ) ).
environment->insert_test_data( i_data = vbak ).
knvv = VALUE #( ( kunnr = '0000000104' vkorg = '2010' vtweg = '10' spart ='10' kvgr5 = 'A15' kvgr1 = 'Z01' ) ).
environment->insert_test_data( i_data = knvv ).
vbfa = VALUE #( ( vbelv = '1000000258' posnv = '000010' vbtyp_n = 'J' vbeln = '2000000283' posnn = '000010' ) ).
environment->insert_test_data( i_data = vbfa ).
likp = VALUE #( ( vbeln = '2000000283' podat ='00000000' ) ).
environment->insert_test_data( i_data = likp ).
tasalesofc = VALUE #( ( sales_ofc = 'A15' tax_ofc_num = '01145' ) ).
environment->insert_test_data( i_data = tasalesofc ).
tataxofc_t = VALUE #( ( tax_ofc_num = '01145' spras = 'E' tax_ofc_name = 'Test TaxOf' ) ).
environment->insert_test_data( i_data = tataxofc_t ).
tvv5t = VALUE #( ( kvgr5 = 'A15' spras = 'E' bezei = 'TEST Purpose' ) ).
environment->insert_test_data( i_data = tvv5t ).
" Execute the CUT which in turn will fetch the data from the doubles and
" then validate the output
SELECT * FROM i_asalesorddocflwcustsale
INTO TABLE @DATA(lt_so_cust_data).
cl_abap_unit_assert=>assert_equals( act = lines( lt_so_cust_data )
exp = 1 ).
ENDMETHOD.
ENDCLASS.
Case 2. CUT with tables and other CDS views as its DOC.
All steps are same as above except the below ones –
iv) The data declaration for CDS views should be of type of the corresponding SQL view.
iv) a). In the class_setup fixture method the DOC will be declared of type ‘CDS_VIEW’
iv) b). Data will be populated in the CDS view instead of the table.(Syntax wise it remains the same as above).
As always, feedbacks and mutual learnings are most welcome.