Friday, 6 July 2018

Introducing: GLADIUS – A Test Unit Framework

Why GLADIUS?


Gladius is the name of the famous roman sword. Coding and fighting has a lot of similiarities: you have to work hard to become a good programmer, you have to practice, you need good tools. That’s why we thought that GLADIUS might be a good and appreciable name for a learning tool.

What is it all about?


Codewars or Codefights challenges you to write a function that returns the right result for a given input. You can code in many different languages like C++, Javascript, Ruby, php and others.

The function can be faulty so that you have to correct it or it can be completely empty so that you will have to code the complete function to return the correct result.

Unit tests

Your result will be checked by plenty of given unit tests. If there are wrong tests you must correct the function. If all tests are passed, you won the challenge.

There are visible and hidden tests. For visible tests you can see the input and the result. Hidden tests only tell you if it passed or not.

How do I know the right results?


There are mainly two types of challenge:

a) You will be given a detailed description of the problem. You will get examples and a lot of hints what the function should do and what not.

b) You only have unit test. You must derive the functionality by the results given in the unit tests. My favourite is this one:

1 = pump
5 = pump
10 = feet
12 = stomping

What are the results for 2, 3, 4…?

Back to ABAP


With this blog series I will try to tell you step by step what we are planning and what are the basics. At the end we dream of a magic workbench where complete workshops or courses can be defined, & tested, where you can create statistics and define or measure metrics and many more.

The following steps will show you the most simple framework to starting something like ABAP code fights.

The ingredients

All of the following objects are fully integrated in the ABAP workbench or ABAP in Eclipse. We will use the integrated ABAP Test Cockpit functionality.

These are the objects we need:

◈ Interface which defines the method (importing and returning parameters)
◈ A helper class to access different solution classes
◈ A master class which hold the complete solution of the functionality
◈ A class with all test units

The Interface

Define an interface with a simple method TEST_ME and the importing parameter IN and the returning parameter OUT.

INTERFACE zif_glds_demo_test
  PUBLIC .

  METHODS test_me
    IMPORTING
      !in        TYPE i
    RETURNING
      VALUE(out) TYPE i .
ENDINTERFACE.

This interface will be embedded in all solution classes that someone would like to create.

The Helper

The target is to have one class with test units and many solution classes of different people. But we do not want to implement unit tests to every solution class. We will create one super test unit class, the test master, and derive from this class.

To have access to the very class of the particular solution, we need a trick which we will implement in the helper class:

CLASS zcl_glds_demo_test_helper DEFINITION
  PUBLIC
  ABSTRACT
  CREATE PUBLIC .

  PUBLIC SECTION.

    DATA mo_class_to_test_generic TYPE REF TO object .

    METHODS constructor .
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zcl_glds_demo_test_helper IMPLEMENTATION.


* <SIGNATURE>-------------------------------------------------------------+
* | Instance Public Method ZCL_GLDS_DEMO_TEST_HELPER->CONSTRUCTOR
* +--------------------------------------------------------------------------------+
* +-----------------------------------------------------------------------------</SIGNATURE>
  METHOD constructor.

    TRY.
        DATA(lv_classname) = cl_abap_classdescr=>get_class_name( me ).
        FIND REGEX '\\PROGRAM=([^=]+)' IN lv_classname SUBMATCHES lv_classname.
        lv_classname = lv_classname(30).
        CREATE OBJECT me->mo_class_to_test_generic TYPE (lv_classname).
      CATCH cx_root.
    ENDTRY.

    IF me->mo_class_to_test_generic IS NOT BOUND.
      MESSAGE a000(oo) WITH 'unable to create class to test' lv_classname.
    ENDIF.

  ENDMETHOD.
ENDCLASS.

The class has the generic attribute MO_CLASS_TO_TEST_GENERIC. In the CONSTRUCTOR we will match the respective class to the test object.

The Test Units

Now that we have the helper class, we can code the test units. This test unit class derives from the helper class.

It is important that this class is abstract and that it is a test unit class!

SAP ABAP Certifications, SAP ABAP Learning, SAP ABAP Study Materials

The test methods (= unit tests) must be public and have the flag “testmethod” activated:

SAP ABAP Certifications, SAP ABAP Learning, SAP ABAP Study Materials

CLASS zcl_glds_demo_test_units DEFINITION
  PUBLIC
  INHERITING FROM zcl_glds_demo_test_helper
  ABSTRACT
  CREATE PUBLIC
  FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS .

  PUBLIC SECTION.

    METHODS constructor .
    METHODS test_0      FOR TESTING .
    METHODS test_1      FOR TESTING .
    METHODS test_2      FOR TESTING .
    METHODS test_3      FOR TESTING .
    METHODS test_10     FOR TESTING .
    METHODS test_99     FOR TESTING .
    METHODS test_random FOR TESTING .
  PROTECTED SECTION.
  PRIVATE SECTION.
    DATA mo_class_to_test TYPE REF TO zif_glds_demo_test .
ENDCLASS.

CLASS zcl_glds_demo_test_units IMPLEMENTATION.

  METHOD constructor.
    super->constructor( ).
    mo_class_to_test ?= mo_class_to_test_generic.
  ENDMETHOD.

  METHOD test_0.
    cl_abap_unit_assert=>assert_equals(
      exp = 0
      act = mo_class_to_test->test_me( 0 )
      msg = '0 should be 0' ).
  ENDMETHOD.

  METHOD test_1.
    cl_abap_unit_assert=>assert_equals(
      exp = 0
      act = mo_class_to_test->test_me( 1 )
      msg = '1 should be 0' ).
  ENDMETHOD.

  METHOD test_10.
    cl_abap_unit_assert=>assert_equals(
      exp = 90
      act = mo_class_to_test->test_me( 10 )
      msg = '10 should be 90' ).
  ENDMETHOD.

  METHOD test_2.
    cl_abap_unit_assert=>assert_equals(
      exp = 2
      act = mo_class_to_test->test_me( 2 )
      msg = '2 should be 2' ).
  ENDMETHOD.

  METHOD test_3.
    cl_abap_unit_assert=>assert_equals(
      exp = 6
      act = mo_class_to_test->test_me( 3 )
      msg = '3 should be 6' ).
  ENDMETHOD.

  METHOD test_99.
    cl_abap_unit_assert=>assert_equals(
      exp = 9702
      act = mo_class_to_test->test_me( 99 )
      msg = '99 should be 9702' ).
  ENDMETHOD.

  METHOD test_random.
    DATA(rnd) = cl_abap_random_int=>create( seed = 42 min = 10 max = 10000 )->get_next( ).
    DATA(result) = rnd * ( rnd - 1 ).
    cl_abap_unit_assert=>assert_equals(
      exp = result
      act = mo_class_to_test->test_me( rnd )
      msg = |{ rnd } should be { result }| ).
  ENDMETHOD.
ENDCLASS.

Make sure that the MO_CLASS_TO_TEST attribute referrs to the test interface.

The class to test inside the test units must be the MO_CLASS_TO_TEST attribute!

The Test Master

The test master class only implements the interface to have the structure of the test method and provides the correct functionality.

CLASS zcl_glds_demo_test_master DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    INTERFACES zif_glds_demo_test .
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zcl_glds_demo_test_master IMPLEMENTATION.

* <SIGNATURE>------------------------------------------------------------------+
* | Instance Public Method ZCL_GLDS_DEMO_TEST_MASTER->ZIF_GLDS_DEMO_TEST~TEST_ME
* +----------------------------------------------------------------------------------+
* | [--->] IN                             TYPE        I
* | [<-()] OUT                            TYPE        I
* +-----------------------------------------------------------------------------</SIGNATURE>
  METHOD zif_glds_demo_test~test_me.

    IF in = 0.
      out = 0.
      RETURN.
    ENDIF.

    out = in * ( in - 1 ).

  ENDMETHOD.
ENDCLASS.

The function simply multiplies the given value by itself minus one.

Note that this class has local test units! But these test units are not implemented in this class but are derived from the global test units class.

CLASS lcl_test DEFINITION
  INHERITING FROM zcl_glds_demo_test_units
  FOR TESTING
  RISK LEVEL HARMLESS
  DURATION SHORT.

ENDCLASS.

Ready Player 1


This is the most simple framework to start implementing your own function:

Of course someone who wants to implement the function should not see the code of the master solution! You can provide a class which implements the interface and derives from the global test class.

Simply create a class like ZCL_GLDS_DEMO_SOLUTION_1, implement the interface ZIF_GLD_DEMO_TEST and create a local test unit section like the one above.

After activating all that stuff and running the unit tests via CTRL-SHIFT-F10 you will see that there are errors.

SAP ABAP Certifications, SAP ABAP Learning, SAP ABAP Study Materials

Next…


Maybe you can imagine that there is a lot of potential in this…

At least the administration design should look something like this:

SAP ABAP Certifications, SAP ABAP Learning, SAP ABAP Study Materials

Administrators can define different projects for workshops with different tasks. These can be tasks to new ABAP features, mathematical problems or customer specific functions.

Users can register for a project. Team leaders can see their achievements in overviews.

Test classes might be generated automatically.

The source code might be stored as a string in the database instead of a global class in the repository.

There might be challenges: Who provides the fastest solution to a task? Who uses the least number of statements?

Book authors might enrich their examples with GLADIUS test cases so that learners can interactively practice the books theory.

We could also think of a defined function (test interface) and the administrator simply enters the input values and the expexted results in the application. No need for programmed test class. This could bring test driven development to a complete new level. Imagine that your key users “write” the test cases… 

No comments:

Post a Comment