Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
former_member182680
Active Participant

Introduction

Unit Tests are an integral part of the software development lifecycle. The purpose of tests is to ensure the proper functionality of a system. Also, if changes are made to specific parts of the system, Unit Tests can immediately show if any functionality has been broken during the last update.

A common problem in unit testing is to separate the system under test, which is subject to the unit test, from other parts which are not to be tested. Common parts, which need to be separated and replaced specifically for unit tests are usually business object repositories or database access components as well as components which rely on third party systems.

This blog is not about unit testing in ABAP. This blog is about developing them faster.

Scenario

For demo purposes, I build up a scenario. It includes a simple flight observer, which has access to a flight information system.

Furthermore, it has access to an alert component which raises mail, SMS etc. alerts.

Both components are dependencies on which the observer relies on. Both components will need to be replaced by mockups for testing purpose since they are not subject to the unit test. Rather, the flight observer is to be tested, in the following sections referred to as the system under test (SUT).

The SUT is implemented by /LEOS/CL_FLIGHT_OBSERVER.

The flight information component is described by the interface /LEOS/IF_IS_IN_TIME_INFO.

The alert processor is described by the interface /LEOS/IF_FLIGHT_ALERT_PROCESS.

The validation logic is very simple: It decides, weather an alert for a specific flight is raised or not. Every delay with more than 60 minutes is getting raised. However, in any other real scenario, there wouldn’t be such a simple hard coded routine but for demo purposes it is fine.
All dependencies to the two other components are set via its constructor using dependency injection.

Unit Test realization Part I

The method which is going to be tested is described below and implements the 60-minutes rule: every delay of more than 60 minutes is getting delegated to the alert system.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method /LEOS/CL_FLIGHT_OBSERVER->OBSERVE_FLIGHT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_CARRID                      TYPE        S_CARR_ID
* | [--->] IV_CONNID                      TYPE        S_CONN_ID
* | [--->] IV_FLDATE                      TYPE        S_DATE
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD observe_flight.
  DATA lv_delay_minutes TYPE i.
lv_delay_minutes = mo_is_in_time_access->get_delay( iv_carrid = iv_carrid iv_connid = iv_connid iv_fldate = iv_fldate ).
  IF lv_delay_minutes > 60.
mo_alert_processor->alert_delay( iv_carrid = iv_carrid iv_connid = iv_connid iv_fldate = iv_fldate ).
  ENDIF.
ENDMETHOD.


This implementation is not a challenge at all. More tedious is the implementation of local classes which are implementing the functionality of the two components on which the SUT depends. These implementations are called mocks.

The flight information access, implementing /LEOS/IF_IS_IN_TIME_INFO, would have to return flights which are sometimes in time and sometimes not. This flight information would have to be hard coded in the unit test. It might look like this:

CLASS lcl_flight_info DEFINITION.
  PUBLIC SECTION.
    INTERFACES /leos/if_is_in_time_info.
ENDCLASS.                    "lcl_flight_info DEFINITION
CLASS lcl_flight_info IMPLEMENTATION.
  METHOD /leos/if_is_in_time_info~get_delay.
    IF iv_carrid = 'LH' AND iv_connid = 402 AND iv_fldate = '20121109'.
      rv_delay = 100.
    ENDIF.
    IF iv_carrid = 'LH' AND iv_connid = 402 AND iv_fldate = '2012110'.
      rv_delay = 5.
    ENDIF.
ENDMETHOD. "/LEOYM/if_is_in_time_info~get_delay
ENDCLASS.                    "lcl_flight_info IMPLEMENTATION


There would be another mockup for the alert component, tracking the number of calls made to the method ALERT_DELAY( ). We do not show it here.

The setup routine for the unit test to setup all the required objects would look like this:

  METHOD setup.
*   this call creates the flight information mockup
    CREATE OBJECT mo_is_in_time_access TYPE lcl_flight_info.
*   create an empty alert backend (we just need to track the number of method calls)
    CREATE OBJECT mo_alert_processor_mocker TYPE lcl_alert_process.
*   create the flight observer which is subject to this test
    CREATE OBJECT mo_system_under_test
      EXPORTING
io_alert_processor = mo_alert_processor
        io_in_time_access  = mo_is_in_time_access.
ENDMETHOD. "setup

The test now calls the SUT with various flight data. Some data will lead to a delay, like flight LH / 402 / in November 9th 2012, some will not (like LH / 402 in November 10th 2012). The test is going to evaluate if the alert processor has been called or not, depending on the requested flight data.

Unit Test realization Part II

The described approach requires  a lot of coding to implement the mockups. This coding is never ever reused and basically a waste of time.

This is where mocking frameworks come into place.

Mocking Frameworks are not meant for productive usage, however, their only usage is to save coding in unit tests, hence, development efforts.

The same mockup, which returns flight information data could be mocked using a mocking framework:

    DATA lo_is_in_time_access TYPE REF TO /leos/if_is_in_time_info .
    DATA lo_is_in_time_mocker TYPE REF TO /leos/if_mocker.
    DATA lo_mocker_method TYPE REF TO /leos/if_mocker_method.
*   create the flight information backend
lo_is_in_time_mocker = /leos/cl_mocker=>/leos/if_mocker~mock( iv_interface = '/LEOS/IF_IS_IN_TIME_INFO' ).
lo_mocker_method = lo_is_in_time_mocker->method( 'GET_DELAY' )."mock method GET_DELAY
lo_mocker_method->with( i_p1 = 'LH' i_p2 = 402 i_p3 = '20121109' )->returns( 100 )."flight LH / 402 in November 9th is not in time
lo_mocker_method->with( i_p1 = 'LH' i_p2 = 402 i_p3 = '20121110' )->returns( 5 )."flight LH / 402 in November 10th is in time
*   this call creates the flight information mockup
lo_is_in_time_access ?= mo_is_in_time_mocker->generate_mockup( ).


The whole mockup implementation is described by only two lines of ABAP coding. Three more lines include data declarations and one line actually generates the mock object for you. This sums up to 8 lines of code, in comparison to 14 lines of code which are needed for the manual implementation.

The mockup for the alert processor is even more simple, since its only purpose is to track the number of calls made against method ALERT_DELAY. This feature is already included in the mocking framework as described in the test coding utilizing these mocks. You would just need to call method HAS_METHOD_BEEN_CALLED( … ) on the corresponding mocker object.

    DATA lo_alert_processor_mocker TYPE REF TO /leos/if_mocker.
    DATA lo_alert_processor TYPE REF TO /leos/if_mocker_method.
*   create an empty alert backend (we just need to track the number of method calls)
lo_alert_processor_mocker = /leos/cl_mocker=>/leos/if_mocker~mock( iv_interface = '/LEOS/IF_FLIGHT_ALERT_PROCESS' ).
*   this call creates the alert processor mockup
lo_alert_processor ?= lo_alert_processor_mocker->generate_mockup( ).


Unit Test coding

REPORT /leos/_test_flight_observer.
*----------------------------------------------------------------------*
*       CLASS lcl_test_observer DEFINITION
*----------------------------------------------------------------------*
*
*----------------------------------------------------------------------*
CLASS lcl_test_observer DEFINITION FOR TESTING.
  "#AU Risk_Level Harmless
  "#AU Duration Short
  PROTECTED SECTION.
    DATA mo_is_in_time_access TYPE REF TO /leos/if_is_in_time_info .
    DATA mo_is_in_time_mocker TYPE REF TO /leos/if_mocker.
    DATA mo_alert_processor TYPE REF TO /leos/if_flight_alert_process .
    DATA mo_alert_processor_mocker TYPE REF TO /leos/if_mocker.
    DATA mo_system_under_test TYPE REF TO /leos/cl_flight_observer.
  PRIVATE SECTION.
    METHODS setup.
    METHODS teardown.
    METHODS test_no_alert FOR TESTING.
    METHODS test_with_alert FOR TESTING.
ENDCLASS.                    "lcl_test_observer DEFINITION
*----------------------------------------------------------------------*
*       CLASS lcl_test_observer IMPLEMENTATION
*----------------------------------------------------------------------*
*
*----------------------------------------------------------------------*
CLASS lcl_test_observer IMPLEMENTATION.
  METHOD setup.
    DATA lo_mocker_method TYPE REF TO /leos/if_mocker_method.
*   create the flight information backend
mo_is_in_time_mocker = /leos/cl_mocker=>/leos/if_mocker~mock( iv_interface = '/LEOS/IF_IS_IN_TIME_INFO' ).
lo_mocker_method = mo_is_in_time_mocker->method( 'GET_DELAY' ).
lo_mocker_method->with( i_p1 = 'LH' i_p2 = 402 i_p3 = '20121109' )->returns( 100 ).
    lo_mocker_method->with( i_p1 = 'LH' i_p2 = 402 i_p3 = '20121110' )->returns( 5 ).
*   this call creates the flight information mockup
mo_is_in_time_access ?= mo_is_in_time_mocker->generate_mockup( ).
*   create an empty alert backend (we just need to track the number of method calls)
mo_alert_processor_mocker = /leos/cl_mocker=>/leos/if_mocker~mock( iv_interface = '/LEOS/IF_FLIGHT_ALERT_PROCESS' ).
*   this call creates the alert processor mockup
mo_alert_processor ?= mo_alert_processor_mocker->generate_mockup( ).
*   create the flight observer which is subject to this test
    CREATE OBJECT mo_system_under_test
      EXPORTING
io_alert_processor = mo_alert_processor
io_in_time_access  = mo_is_in_time_access.
ENDMETHOD. "setup
  METHOD teardown.
ENDMETHOD. "teardown
  METHOD test_no_alert.
mo_system_under_test->observe_flight( iv_carrid = 'LH' iv_connid = 402 iv_fldate = '20121110' ).
cl_aunit_assert=>assert_initial( mo_alert_processor_mocker->has_method_been_called( 'ALERT_DELAY' ) ).
ENDMETHOD. "test_no_alert
  METHOD test_with_alert.
mo_system_under_test->observe_flight( iv_carrid = 'LH' iv_connid = 402 iv_fldate = '20121109' ).
    cl_aunit_assert=>assert_not_initial( mo_alert_processor_mocker->has_method_been_called( 'ALERT_DELAY' ) ).
ENDMETHOD. "test_with_alert
ENDCLASS.                    "lcl_test_observer IMPLEMENTATION

Conclusion

This was only a simple example. But even this example showed how easy about 40% of code required to mock objects could be saved. What about real world scenarios with even more complex dependencies and behaviour? In fact, 40% of coding can be saved also in these projects, often even more. Most objects can be easily mocked using a mocking framework if they are really treated as a black box.

What a good mocking framework should also provide, is mocking of specific classes using inheritance. Also, exceptions might need to be registered for specific inputs.

Currently, the described mocking framework is still under development. It supports mocks based Interfaces, method call observation and raising of exceptions but not yet the mocking based on already existing classes. However, it is in productive usage in our projects and has already been extremely valuable for making unit tests simpler and easier to implement.

Further blogs may focus on how to manage dependencies in big development projects and how dependency injection might work in ABAP using an IoC Container. Stay tuned.

5 Comments