Motivation
By now we have all come to recognize the advantages of unit testing our code and have accepted it as a non-negotiable part of the development process. But with the paradigm shift to push our code to the database, we have been unceremoniously blasted back to the dark ages of software testing. We are now faced with the problem of how to test our code pushdown logic implemented in ABAP CDS entities.
CDS Test Double Framework enables developers to test the logic expressed in their CDS entities in an automated way using the well-known ABAP Unit Test Framework.
Challenges
Since the logic in a CDS entity is executed in the underlying database (independent of the ABAP runtime), it is not possible to use conventional ABAP solutions for dependency injection. The depended-on components (DOC) of the entity need to be doubled in the database and we have to ensure that the database engine calls/executes these doubles instead of the depended-on components when the CDS entity is tested.
In order to be able to test the logic in the CDS entity under test (CUT) in a controlled way we further have to provide a possibility to inject test-specific data into the CUT via the double. This means that it must be possible to insert test-data into the double so that this data can be returned by the doubles when the CUT is executed. This is particularly challenging for depended-on components which are inherently read-only in the context of ABAP CDS, e.g. database views and database functions.
Important acronyms used through-out the post are:
CUT = CDS entity Under Test
DOC = Depended-On Component
CDS Test Double Framework
The CDS Test Double Framework addresses the challenges above and enables unit testing of CDS entities by automatically:
1. Creating temporary updatable doubles for each depended-on component in the same DB schema. In the CDS Test Double Framework, test doubles are created as stubs. Stubs provide the desired test data to the CUT. The doubles will have the same structure as the original depended-on components:
- Depended-on database tables are copied without data. Primary key constraints of the depended-on database tables are not copied. This allows you to easily insert test data into the double without having to worry about data integrity. Similarly, database indexes are also not copied.
- Database tables are created for depended-on database views. These tables have the same structure as the depended-on database views.
- Depended-on database functions (resulting from depended-on CDS views with parameters and depended-on CDS table functions) are copied and the implementation of the function double is modified to be able to insert the desired test-data into the double.
What to Test?
Unit tests should focus on checking dedicated non-trivial functions defined and implemented by a given view. Not every CDS view requires a unit test. Before implementing unit tests for CDS view it is advisable to identify the aspects of the entities which are relevant for testing.
In general, unit tests should be implemented for entities which contain some measure of code pushdown. Potential candidates for testing could be the following:
Calculations and/or filters, conversions, conditional expressions like CASE…THEN…ELSE or COALESCE, type changing CAST operations, cardinality changes or checks against NULL values, JOIN behavior, complex where conditions etc.,
Unit tests should not be used to test the properties of CDS entities which might be better tested using static checks, integration tests, etc. or if they do not provide any beneficial value like for a simple CDS projection views.
Tests which are based on the CDS Test Double Framework are not suitable for performance tests.
How to write unit tests using CDS Test Double Framework
In the following section, we will create unit tests for the following CDS view using the widely used ABAP Unit Test Framework itself.
@AbapCatalog.sqlViewName: 'zSo_Items_By_1'
@EndUserText.label: 'Aggregations/functions in SELECT list'
@AbapCatalog.compiler.compareFilter: true
define view Salesorder_Items_By_TaxRate
as select from CdsFrwk_Sales_Order_Item
association [1] to snwd_so as _sales_order on so_guid = _sales_order.node_key
{
so_guid,
coalesce ( _sales_order.so_id, '9999999999' ) as so_id,
currency_code,
sum( gross_amount ) as sum_gross_amount,
tax_rate,
_sales_order
}
group by
so_guid,
_sales_order.so_id,
currency_code,
tax_rate
Create an ABAP Test class
Create an ABAP Test Class to unit test the CDS view. It is a good practice to use the same/similar name of the CUT and prefix it with _TEST for the Test class.e.g. for a CDS Salesorder_Items_By_TaxRate, the test class could be Salesorder_Items_By_TaxRate_Test.
Since the unit tests and the CDS are different artifacts, same/similar names help in easily searching for the related tests.
CLASS Salesorder_Items_By_TaxRate_Test DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
...
...
ENDCLASS.
CLASS SO_ITEMS_BY_TAXRATE_TEST IMPLEMENTATION.
...
...
ENDCLASS.
Define Fixture Methods
Define the following setup and teardown methods.
Executing the method cl_cds_test_environment=>create( i_for_entity = ‘<CDS under test>’ ), implicitly creates all the depended-on component test doubles in the database. This method should be called only once in the test class.
"Fixture method class_setup is executed only once in the beginning of the execution of test class
METHOD class_setup.
"For parameter i_for_entity, specify the CDS view to be unit tested. This will create all the depended-on component Test doubles in the database.
environment = cl_cds_test_environment=>create( i_for_entity = 'Salesorder_Items_By_TaxRate' ).
ENDMETHOD.
METHOD class_teardown.
environment->destroy( ).
ENDMETHOD.
"Fixture method setup is executed once before each test method execution
<cod
METHOD setup.
environment->clear_doubles( ).
ENDMETHOD.
Define a unit test method
METHOD cuco_1_taxrate_1_item_1_ok.
ENDMETHOD.
Prepare Inputs – Insert Test Data in doubles
METHOD cuco_1_taxrate_1_item_1_ok.
"Step 1 : Insert testdata into the doubles
"Step 1.1 : create an instance of type snwd_so. Note : CDS view Salesorder_Items_By_TaxRate depends on snwd_so.
sales_orders = VALUE #( ( client = sy-mandt node_key = '01' so_id = 'ID' ) ).
"Step 1.2 : Use the framework method CL_CDS_TEST_DATA=>create(..) to create the test_data object
test_data = cl_cds_test_data=>create( i_data = sales_orders ).
"Step 1.3 : Use the framework method environment->get_double(..) to create the instance of the double 'SNWD_SO'
DATA(sales_orders_double) = environment->get_double( i_name = 'SNWD_SO' ).
"Step 1.4 : Insert the testdata into the double depended-on component object
sales_orders_double->insert( test_data ).
"Repeat Step 1 for all the depended-on component doubles
sales_order_items = VALUE #( ( mandt = sy-mandt so_guid = '01' currency_code = 'EUR' gross_amount = '1' tax_rate = '19.00' ) ).
test_data = cl_cds_test_data=>create( i_data = sales_order_items ).
DATA(sales_order_items_double) = environment->get_double( i_name = 'CdsFrwk_DEMO_1' ).
sales_order_items_double->insert( test_data ).
...
ENDMETHOD.
Execute CDS
SELECT * FROM cdsfrwk_so_items_by_taxrate INTO TABLE @act_results.
Verify output – Assert using ABAP Unit Test APIs
exp_results = VALUE #( ( so_id = 'ID' currency_code = 'EUR' sum_gross_amount = '1' tax_rate = '19.00' ) ).
cl_abap_unit_assert=>assert_equals(
act = lines( act_results )
exp = lines( exp_results ) ).
The method looks as follows:
METHOD cuco_1_taxrate_1_item_1_ok.
"Step 1 : Insert testdata into the doubles
"Step 1.1 : create an instance of type snwd_so
sales_orders = VALUE #( ( client = sy-mandt node_key = '01' so_id = 'ID' ) ).
"Step 1.2 : Use the framework method CL_CDS_TEST_DATA=>create to create the test_data object
test_data = cl_cds_test_data=>create( i_data = sales_orders ).
"Step 1.3 : Use the framework method environment->get_double to the instance of the DOC double 'SNWD_SO'
DATA(sales_orders_double) = environment->get_double( i_name = 'SNWD_SO' ).
"Step 1.4 : Insert the testdata into the DOC double object
sales_orders_double->insert( test_data ).
"Repeat Step 1 for all the DOC doubles
sales_order_items = VALUE #( ( mandt = sy-mandt so_guid = '01' currency_code = 'EUR' gross_amount = '1' tax_rate = '19.00' ) ).
test_data = cl_cds_test_data=>create( i_data = sales_order_items ).
DATA(sales_order_items_double) = environment->get_double( i_name = 'CdsFrwk_DEMO_1' ).
sales_order_items_double->insert( test_data ).
"Step 2 : Execute the CDS
SELECT * FROM cdsfrwk_so_items_by_taxrate INTO TABLE @act_results.
"Step 3 : Verify Expected Output
exp_results = VALUE #( ( so_id = 'ID' currency_code = 'EUR' sum_gross_amount = '1' tax_rate = '19.00' ) ).
assert_so_items_by_taxrate( exp_results = exp_results ).
ENDMETHOD.
Running Unit tests for CDS
In ABAP Development tools, open the ABAP Test class containing all the unit tests for CDS. Right-click and choose Run As -> ABAP Unit Test, or hit ctrl+shift+F10 to run the unit tests. The results are shown in the ABAP Unit Runner eclipse view.
Note: Running the tests being in the DDL source editor is not possible as of now.
Supported Test scenarios
CDS Test Double framework supports creation of test doubles for the following depended-on components (DOCs) for a given CDS View under test (CUT):
- DDIC tables
- DDIC views
- CDS views
- CDS views with Parameters
- External Views
- Table Functions
- CDS special functions. CURRENCY_CONVERSION and UNIT_CONVERSION
You can also turn on/off the DCL for a given CDS. More details later are provided later in this post.
Depended-on Component is Table Function
Doubles of type Tables Functions are handled in the same manner as any CDS view
Depended-on Component is CDS View with parameters
CDS Test Double Framework offers
cl_cds_test_data=>create( .. )->for_parameters( .. )
to insert test data into double of types View with parameters.
METHOD eur_tax_rate_19_found.
"Step 1 : Insert testdata into the doubles
open_items = VALUE #( ( mandt = sy-mandt so_guid = '0F' tax_rate = '19.00' so_id = '1' ) ).
i_param_vals = VALUE #( ( parm_name = `pCuCo` parm_value = `EUR` ) ).
"CdsFrwk_demo_3 is a CDS view with parameters. Use framework method ->for_parameters( ) to insert test data
test_data = cl_cds_test_data=>create( i_data = open_items )->for_parameters( i_param_vals ).
DATA(open_items_double) = environment->get_double( 'CdsFrwk_demo_3' ).
open_items_double->insert( test_data ).
...
...
ENDMETHOD.
DCL affects CDS under test
You can also turn on/off the DCL for a given CDS. But we recommend that for now, you always run your tests by disabling the DCL if there is a DCL acting on your CDS DDL under test. In future, there would be lot of options to play around by enabling the DCL and using the role authorization doubles etc. But with this first release, you can focus on testing the CDS DDL by disabling the DCL completely if there is one. Writing tests by enabling the DCL could result in tests failing intermittently since the actual access control role authorizations would be applied. Hence, it is always recommended to have DISABLE_DCL=ABAP_TRUE in the cl_cds_test_environment=>create(…) method for your productive tests.
Support for Special Functions : CURRENCY_CONVERSION and UNIT_CONVERSION
CDS Test Double framework offer means to create test doubles for the CDS special function CURRENCY_CONVERSION and UNIT_CONVERSION.
"Step 1 : Create testdata using the special framework method create_currency_conv_data
test_data = cl_cds_test_data=>create_currency_conv_data( output = '399.21' )->for_parameters(
amount = '558.14'
source_currency = 'USD'
target_currency = 'EUR'
exchange_rate_date = '20150218'
).
"Step 2 : Get the double instance using the framework method get_double
DATA(curr_conv_data_double) = environment->get_double( cl_cds_test_environment=>currency_conversion ).
"Step 3 : Insert test_data into the double
curr_conv_data_double->insert( test_data ).
Test Doubles with NULL Values
To insert NULL values into the test double, CDS Test Double Framework offers the method cl_cds_test_data=>create( .. )->set_null_values( .. ) to explicitly set NULL values.
partners = VALUE #( ( client = sy-mandt bp_id = '1' ) ).
"Step 1 : define the list of columns into which NULL is inserted
i_null_vals = VALUE #( ( `address_guid` ) ).
"Step 2 : Create testdata and set the NULL value object
test_data = cl_cds_test_data=>create( i_data = partners )->set_null_values( i_null_vals ).
"Step 3 : Get test Double instance
DATA(partners_double) = environment->get_double( i_name = 'SNWD_BPA' ).
"Step 4 : Insert test data into test double
partners_double->insert( test_data ).
No comments:
Post a Comment