Monday, 6 August 2018

GLADIUS – The Next Level

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:

SAP ABAP Development, SAP ABAP Guides, SAP ABAP Study Materials, SAP ABAP Certifications

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:

SAP ABAP Development, SAP ABAP Guides, SAP ABAP Study Materials, SAP ABAP Certifications

The Editor


Report ZGLDS_DEMO_GENERATE_SOLUTION

SAP ABAP Development, SAP ABAP Guides, SAP ABAP Study Materials, SAP ABAP Certifications

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.

SAP ABAP Development, SAP ABAP Guides, SAP ABAP Study Materials, SAP ABAP Certifications

1 comment:

  1. your blog is really useful for me. Thanks for sharing this useful blog..sap-hr abap training


    ReplyDelete