this is the 2nd part of the GLADIUS-series. the first part is here:
Introducing GLADIUS - A Test Unit Framework
What Happened Before
I will give you a short overview of the current progress of the test framework. GLADIUS is the project name of something that some day will be similar to
codefights or
codewars - but for ABAP.
In the introduction part I developed the basics for testing various classes with one global test unit class. In general you can use a global test class as
setup for derived local test classes. But you cannot test
any other class with this global test units. That's why we created a helper class. With this helper class we can mock any other class (assumed that the correct initerface is implemented...!) to the global test class.
To use this specific test setup, you wil have to create a class with some specific conditions. In this blog post I will show you how to make this easier.
Roadmap
In this level we will do the following things in order to directly code the solution and see the results next to the code.
- Create a template which contains all specific test issues
- Create a test runner to test a report containing a test class
- Create the editor
- Call the unit test runner and display the results
From Global To Local
Up to now we used a global class to test. This is okay, but using a report that contains a test class is better because it can easily be generated and used for inital setup.
A template in the shape of a report is very easy and simple:
REPORT zglds_demo_template.
CLASS lcl_solution DEFINITION.
PUBLIC SECTION.
INTERFACES zif_glds_demo_test .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS lcl_solution IMPLEMENTATION.
METHOD zif_glds_demo_test~test_me.
* declarations
* implementation
ENDMETHOD.
ENDCLASS.
*"* use this source file for your ABAP unit test classes
CLASS lcl_test DEFINITION
INHERITING FROM zcl_glds_demo_test_units
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
ENDCLASS.
To make sure that this works with the helper class we need to adapt the helper's class constructor:
DATA lv_class_local TYPE c LENGTH 100.
TRY.
DATA(lv_classname) = cl_abap_classdescr=>get_class_name( me ).
IF lv_classname(13) = '\PROGRAM=ZCL_'.
FIND REGEX '\\PROGRAM=([^=]+)' IN lv_classname SUBMATCHES lv_classname.
lv_classname = lv_classname(30).
ELSE.
lv_class_local = lv_classname.
"'\PROGRAM=ZGLDS_DEMO_TEMPLATE\CLASS=LCL_SOLUTION'.
REPLACE 'LCL_TEST' WITH 'LCL_SOLUTION' INTO lv_class_local.
lv_classname = lv_class_local.
ENDIF.
CREATE OBJECT me->mo_class_to_test_generic TYPE (lv_classname).
CATCH cx_root.
ENDTRY.
Now we can press CTRL-SHIFT-F10 to execute the unit tests for this program.
Generator
It would now be quite easy to somehow copy an implementation into this template, generate the report and run or check it.
Therefore I found the report DEMO_GENERIC_PROGRAM which I simply copied and adapted a little bit.
This report loads a template and puts the code from the editor between METHOD and ENDMETHOD:
PROGRAM demo_generic_template.
CLASS demo DEFINITION FINAL.
PUBLIC SECTION.
CLASS-METHODS main RAISING cx_static_check cx_dynamic_check.
ENDCLASS.
CLASS demo IMPLEMENTATION.
METHOD main.
* declarations
* implementation
ENDMETHOD.
ENDCLASS.
The report can be syntax checked and executed:
The report generates a temporary subroutine pool and executes the method MAIN. Unfortunately the unit test runner does not like temporary reports...
😞 So we need to use INSERT REPORT.
Unit Test Runner
There is a simple method to execute unit tests for a program or class:
DATA aunit_result TYPE REF TO if_saunit_internal_result.
DATA cvrg_rslt_provider TYPE REF TO if_aucv_cvrg_rslt_provider.
DATA runner TYPE REF TO cl_aucv_test_runner_abstract.
runner = test_runner=>create( passport=>get( ) ).
runner->run_for_program_keys(
EXPORTING
i_limit_on_risk_level = if_aunit_attribute_enums=>c_risk_level-harmless
i_limit_on_duration_category = if_aunit_attribute_enums=>c_duration-short
i_program_keys = VALUE #( ( obj_name = program obj_type = 'PROG' ) )
IMPORTING
e_aunit_result = aunit_result
e_coverage_result = cvrg_rslt_provider ). " can be initial
To use test runner we need to use a class that inherits from CL_AUCV_TEST_RUNNER_ABSTRACT. As this class is abstract we need to implement (redefine) the methods like done in CL_AUCV_TEST_RUNNER_STANDARD. This class is defined as FINAL so taht we cannot use a derivation of this class. We simply copied the implementation:
CLASS test_runner DEFINITION
INHERITING FROM cl_aucv_test_runner_abstract.
PUBLIC SECTION.
CLASS-METHODS create
IMPORTING
!i_passport TYPE REF TO object
RETURNING
VALUE(result) TYPE REF TO test_runner .
METHODS run_for_program_keys REDEFINITION .
METHODS run_for_test_class_handles REDEFINITION .
ENDCLASS.
CLASS test_runner IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_AUCV_TEST_RUNNER_ACP=>CREATE
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_PASSPORT TYPE REF TO OBJECT
* | [<-()] RESULT TYPE REF TO ZCL_AUCV_TEST_RUNNER_ACP
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD create.
DATA passport_name TYPE string.
passport_name = cl_abap_classdescr=>get_class_name( i_passport ).
CREATE OBJECT result.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_AUCV_TEST_RUNNER_ACP->RUN_FOR_PROGRAM_KEYS
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_LIMIT_ON_DURATION_CATEGORY TYPE SAUNIT_D_ALLOWED_RT_DURATION
* | [--->] I_LIMIT_ON_RISK_LEVEL TYPE SAUNIT_D_ALLOWED_RISK_LEVEL
* | [--->] I_PROGRAM_KEYS TYPE SABP_T_TADIR_KEYS
* | [<---] E_COVERAGE_RESULT TYPE REF TO IF_AUCV_CVRG_RSLT_PROVIDER
* | [<---] E_AUNIT_RESULT TYPE REF TO IF_SAUNIT_INTERNAL_RESULT
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD run_for_program_keys.
DATA:
tadir_key TYPE sabp_s_tadir_key,
factory TYPE REF TO cl_aunit_factory,
task TYPE REF TO if_aunit_task,
listener TYPE REF TO if_saunit_internal_listener.
listener = cl_saunit_gui_service=>create_listener( ).
CREATE OBJECT factory.
task = factory->create_task( listener ).
IF ( i_limit_on_risk_level IS NOT INITIAL ).
task->restrict_risk_level( i_limit_on_risk_level ).
ENDIF.
IF ( i_limit_on_duration_category IS NOT INITIAL ).
task->restrict_duration_category( i_limit_on_duration_category ).
ENDIF.
LOOP AT i_program_keys INTO tadir_key.
CASE tadir_key-obj_type.
WHEN 'CLAS'.
task->add_class_pool( tadir_key-obj_name ).
WHEN 'PROG'.
task->add_program( tadir_key-obj_name ).
WHEN 'FUGR'.
task->add_function_group( tadir_key-obj_name ).
WHEN OTHERS.
CONTINUE.
ENDCASE.
ENDLOOP.
task->run( if_aunit_task=>c_run_mode-catch_short_dump ).
e_aunit_result = listener->get_result_after_end_of_task( ).
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_AUCV_TEST_RUNNER_ACP->RUN_FOR_TEST_CLASS_HANDLES
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_LIMIT_ON_DURATION_CATEGORY TYPE IF_AUNIT_TASK=>TY_D_DURATION_CATEGORY
* | [--->] I_LIMIT_ON_RISK_LEVEL TYPE IF_AUNIT_TASK=>TY_D_RISK_LEVEL
* | [--->] I_TEST_CLASS_HANDLES TYPE IF_AUNIT_TEST_CLASS_HANDLE=>TY_T_TESTCLASS_HANDLES
* | [--->] I_CUSTOM_DURATION TYPE IF_AUNIT_TASK=>TY_S_DURATION_SETTING(optional)
* | [--->] I_MODE TYPE IF_AUNIT_TASK=>TY_D_RUN_MODE (default =IF_AUNIT_TASK=>C_RUN_MODE-CATCH_SHORT_DUMP)
* | [--->] I_PACKAGES_TO_MEASURE TYPE STRING_SORTED_TABLE(optional)
* | [<---] E_COVERAGE_RESULT TYPE REF TO IF_AUCV_CVRG_RSLT_PROVIDER
* | [<---] E_AUNIT_RESULT TYPE REF TO IF_SAUNIT_INTERNAL_RESULT
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD run_for_test_class_handles.
DATA:
test_class_handle TYPE REF TO if_aunit_test_class_handle,
factory TYPE REF TO cl_aunit_factory,
task TYPE REF TO if_aunit_task,
listener TYPE REF TO if_saunit_internal_listener.
listener = cl_saunit_gui_service=>create_listener( ).
CREATE OBJECT factory.
task = factory->create_task( listener ).
IF ( i_limit_on_risk_level IS NOT INITIAL ).
task->restrict_risk_level( i_limit_on_risk_level ).
ENDIF.
IF ( i_limit_on_duration_category IS NOT INITIAL ).
TRY.
task->restrict_duration_category( i_limit_on_duration_category ).
CATCH cx_root. "EC *
ENDTRY.
ENDIF.
IF ( i_custom_duration IS NOT INITIAL ).
task->set_duration_limit( i_custom_duration ).
ENDIF.
LOOP AT i_test_class_handles INTO test_class_handle.
task->add_test_class_handle( test_class_handle ).
ENDLOOP.
task->run( i_mode ).
e_aunit_result = listener->get_result_after_end_of_task( ).
ENDMETHOD.
ENDCLASS.
We also need a passport in order to create the runner. No idea why it was implemented this way...
CLASS passport DEFINITION FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS
get
RETURNING VALUE(result) TYPE REF TO passport.
ENDCLASS.
CLASS passport IMPLEMENTATION.
METHOD get.
CREATE OBJECT result.
ENDMETHOD.
ENDCLASS.
The passport used by the SAP test runner makes sure that it can only be used by the standard unit test program. But if it's so easy to fake the passport...
😉
Display Of Test Results
The results of the test unit runner cann be displayed with this short code:
DATA task_data TYPE if_saunit_internal_rt_v3=>ty_s_task.
DATA task_result_casted TYPE REF TO cl_saunit_internal_result.
task_result_casted ?= aunit_result.
CALL FUNCTION '_SAUNIT_CREATE_CTRL_VIEWER_V3'
EXPORTING
task_data = task_result_casted->f_task_data
container = container3.
It is the same view that you get after running the unit tests in the standard:
The Editor
Report ZGLDS_DEMO_GENERATE_SOLUTION
The demo report made differences of declarations and implementation which is not needed. But I wanted to use as much similar to the demo report as possible so I used the same split.
There is the funny fact that ou need to place "DATA var TYPE i" in the declarations window but can use "DATA(var) = 0" in the implementation section.
🙂
Nevertheless you can execute your solution and check if all the unit tests passed.
All Unit Tests Passed?
There is one unlovely issue concerning this presentation of unit tests: If all tests passed the runner only display a success message "all tests passed" but does not show the tests anymore. In real life it is very useful but in our case we want the user to see the tests.
We implemented a simple workaround to make sure that the results are shown:
We added a new unit test method that always is false but where the status is "tolerable":
METHOD test_dummy.
cl_abap_unit_assert=>assert_equals(
exp = 1
act = 0
msg = |Don't worry - This test always fails to display all other results. |
level = if_aunit_constants=>tolerable ).
ENDMETHOD.
That means on the other hand that the result of the test will always be
yellow and never
green. Maybe we find a trick that if all tests passed then the DUMMY-test also is marked as passed but this will be tricky beacuse the unit tests will be executed in a random order.
Ready for Take-Off
Now with this second part of the GLADIUS framework I think it will be even clearer where our way leads us to: a universal test framework that can be used for training, workshops, education and competition.
If there was a maintenance tool around the existing functionality you could built complete sessions with various test cases.
If there were metrics like number of lines of code, execution time, numbers of used commands and others you could start competitions.
If there were customizable restrictions like forbidden commands (IF, LOOP, CHECK etc.) you can force users to find alternative solutions or used new ABAP features.
abapGit
All sources also on github:
https://github.com/tricktresor/gladius
Tribute to
lars.hvam