Unit Tests in ABAP using a mocking framework
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.
Great blog on a subject that's not widely discussed in ABAP. I hope that behind the scenes sap are planning on releasing a mocking framework for ABAP unit just as you've discussed... Please keep the blogs coming. Would be great to see how you've dealt with different types of dependencies. Wondering if you've been trying TDD?
Thank you Jason, that's a good question. We do not yet follow TDD principles consequently, however, from time to time unit tests come before the actual implementation starts.
This is mainly the case when multiple developers are involved in the same component at different layers: It emerged extremely useful of having a unit test ready which already gives a hint of how to use a specific API before talking to an UI developer, even if the unit test is not yet successful. This minimizes the linear work in progress and shifts the development to a more parallel approach
Hi Uwe,
this is the way to go! At some point, also when you use strictly TDD you have to decide to digg deeper into your Tests and to use Mocking-Frameworks. Sadly, there is no Mocking-Framework still available, but I suggest that your Mocking-Framework is more generic and could be used for this purposes?
What do you think, it is possible to publish your Mocking-Framework?
Hello Damir, absolutely, the framework is intended to support any ABAP interface and is not coupled to the interface you have seen in this blog post.
Unfortunately it is currently implemented in our own namespace and still under development. Furthermore, even if we had a Z-Version of it, SAP's CodeExchange wouldn't be an option since companies are not allowed to upload any projects.
Hi Uwe,
the framework sounds good. I didn´t know, that on Code-Exchange such an limitation is there... Maybe it could be interesting to make an google-project?
Anyway, if an open-source-project could start, I would attend as an Developer 🙂