In this document I will show how the ABAP Test Double framework can be used to mock daos and enhance flexible and fast implemented unit tests.
For an introduction to the framework I would recommend ABAP Test Double Framework – An Introduction. and for the usage of daos and dependency injection the following blog: ABAP Unit Tests without database dependency – DAO concept.
The sample below is very basic and no real business logic is included because it focuses on the concepts. For the dependency injection of the daos I used an interface for two reasons. First when more complicated or variants of Daos are needed the interface provides more flexibilty and the ABAP test double framework currently only can mock interfaces.
If you have suggestions, hints or question feel free to comment this document as I am keen on improve my concept, since it will not be the last time I have to write a Dao 🙂
Before starting I want to highlight that this does only focus on the integration of the ABAP test double framework into Dao related testing but does not highlight the extended features the test double provides to enhance your unit tests, like eg. method call verification for methods without return parameters or exception assertion which makes the ABAP test double framework to a very useful tool.
Lets assume we need a class which should get and set information of material data. Lets call it ZCL_MATERIAL_FACADE (because it will represent the facade pattern). For simplicity reason it will only contain one method – get the grossweight for a material by providing a material number.
The access to the database should be hidden behind this class and not contain any business logic – the Material Dao.
For testing we should be independent of the current database state and there should be no need to change productive code. This is accomplished by using a mock or testdouble which is injected into the production code.
There are different approaches to build this set of classes. I assume the simplest is to start with the DAO, continue with the material facade and finally with the mocking classes and unit tests even though tihs does not correlate with the TDD approach.
The DAO in our simplyfied example has one method get which returns one material represented by an row of the database table MARA. The concrete DAO class implements the DAO interface which contains also only one specification for the method get. Important is that the DAO contains a static instance variable which will later be used for injecting the test double (see appendix 1 and 2).
The material facade is quite straightforward (see apendix 3). Only one method getGrossWeight which gets the data over the material DAO interface.
The test double is created over the method cl_aba_testdouble, which needs as parameter an interface – in our example ZIF_MATERIAL_DAO (a class here is not permitted. The methods are configured with the method configure_call and the subsequent call of the method to be stubbed. (see appendix 4).
The exact behavior of the test double in respect to method call and response can be configured on different ways. In the below approach the local class lclAnswerMaterialDaoGet contains this logic (for example the different return grosssweight values for the various materials). To the method stub this local class can be assigned with the method set_answer() (see appendix 5).
The final step to get the business logic tested without database dependencies is shown in the unit test at the end of the appendix. This is done with the following steps:
- The key step is to first create the testdouble and injected to the MaterialDao. As the MaterialDao contains only one instance (singleton pattern) which is accessed from each caller and only instantiate once from now each call on the material dao in the session is done on the material dao test double.
- The second step is to create the material facade and perform the desired tests. It is essential to crate the material facade after the testdoube injection into the material dao, because otherwise the material facade would create the real material dao like in productive environment.
My personal experience with the ABAP test double framework is that the first steps are not easy, especially because you have no possibility to try it out with concrete classes only.
But after a short time you get the invested time back as you have a huge amount of necessary features for testing which are integrated in the ABAP test double framework.
Feel free to share your experience and thougts.
Appendix Listing of all needed interfaces and classes
1. ZIF_DAO_MATERIAL – Common Interface for Material DAO, Mock and TestDouble
interface ZIF_DAO_MATERIAL public . methods GET importing !PMATERIALNUMBER type MATNR returning value(PMARA) type MARA . endinterface.
2. ZCL_DAO_MATERIAL – A simple Dao to access data of a material
class ZCL_DAO_MATERIAL definition public create public . public section. interfaces ZIF_DAO_MATERIAL . class-methods GETINSTANCE returning value(PINSTANCE) type ref to ZIF_DAO_MATERIAL . class-methods SETINSTANCE importing !PINSTANCE type ref to ZIF_DAO_MATERIAL. protected section. private section. CLASS-DATA: instance type ref to ZIF_DAO_MATERIAL, instanceObj type ref to ZCL_DAO_MATERIAL. ENDCLASS. CLASS ZCL_DAO_MATERIAL IMPLEMENTATION. method GETINSTANCE. if ( instance IS INITIAL ). CREATE OBJECT instanceObj. instance ?= instanceObj. endif. pInstance = instance. endmethod. method SETINSTANCE. instance = pInstance. endmethod. method ZIF_DAO_MATERIAL~GET. SELECT * INTO @pMARA FROM MARA WHERE matnr = @pMaterialnumber. ENDSELECT. endmethod. ENDCLASS.
3. ZCL_MATERIAL_FACADE – A simple common facade for accessing material specific data
class ZCL_MATERIAL_FACADE definition public final create public . public section. METHODS: constructor, getGrossWeight importing pMaterialnumber type MATNR returning value(pGrossWeight) type BRGEW. protected section. private section. DATA: materialDao type ref to ZIF_DAO_MATERIAL. ENDCLASS. CLASS ZCL_MATERIAL_FACADE IMPLEMENTATION. method constructor. materialDao = ZCL_DAO_MATERIAL=>getInstance( ). endmethod. method getGrossWeight. DATA: mara type MARA. mara = materialDao->get( pMaterialnumber ). pGrossWeight = mara-brgew. endmethod. ENDCLASS.
4. ZCL_DAO_MATERIAL_TEST_DOUBLE – The test double for simulating the Material Dao
class ZCL_DAO_MATERIAL_TESTDOUBLE definition public final create public for testing . public section. class-methods getTestDouble returning value(PMATERIALDAOTESTDOUBLE) type ref to ZIF_DAO_MATERIAL . protected section. private section. ENDCLASS. CLASS ZCL_DAO_MATERIAL_TESTDOUBLE IMPLEMENTATION. METHOD getTestDouble. DATA: lo_answer type ref to lclAnswerMaterialDaoGet. pMaterialDaoTestDouble ?= cl_abap_testdouble=>create( 'ZIF_DAO_MATERIAL' ). CREATE OBJECT lo_answer. cl_abap_testdouble=>configure_call( pMaterialDaoTestDouble )->ignore_all_parameters( )->set_answer( lo_answer ). pMaterialDaoTestDouble->get( 'DUMMY_ARTIKEL' ). ENDMETHOD. ENDCLASS.
5. LclAnswerMaterialDaoGet – Local Class in ZCL_DAO_MATERIAL_TEST_DOUBLE which contains the logic for the return value
CLASS lclAnswerMaterialDaoGet DEFINITION. public section. interfaces if_abap_testdouble_answer. ENDCLASS. CLASS lclAnswerMaterialDaoGet IMPLEMENTATION. METHOD if_abap_testdouble_answer~answer. DATA: lv_src_materialnumber_data TYPE REF TO data, retMara type MARA. FIELD-SYMBOLS: <lv_src_materialnumber> TYPE MATNR, <retMara> type MARA. lv_src_materialnumber_data = arguments->get_param_importing( 'pMaterialnumber' ). ASSIGN lv_src_materialnumber_data->* TO <lv_src_materialnumber>. if ( <lv_src_materialnumber> = 'EXISTING_ARTIKEL1' OR <lv_src_materialnumber> = 'EXISTING_ARTIKEL2' ). retMara-brgew = 2. else. retMara-brgew = 0. endif. result->set_param_returning( retMara ). endmethod. ENDCLASS.
7. ZCL_DAO_MATERIAL_FACADE_TDOUB_UNIT – Unittest which uses the test double
class ZCL_MATERIAL_FACADE_TDOUB_UNIT definition FOR TESTING. "#AU Risk_Level Harmless PUBLIC SECTION. private section. DATA: materialFacade type ref to ZCL_MATERIAL_FACADE. CLASS-METHODS: class_setup. METHODS: setup, testGetGrossWeightWithTDouble FOR TESTING. ENDCLASS. CLASS ZCL_MATERIAL_FACADE_TDOUB_UNIT IMPLEMENTATION. method class_setup. DATA: materialDaoTestDouble type ref to ZIF_DAO_MATERIAL. materialDaoTestDouble = ZCL_DAO_MATERIAL_TESTDOUBLE=>getTestDouble( ). ZCL_DAO_MATERIAL=>setinstance( materialDaoTestDouble ). endmethod. method setup. create object materialFacade. endmethod. method testGetGrossWeightWithTDouble. DATA: grossWeight type mara-brgew. grossWeight = materialFacade->getGrossWeight( 'EXISTING_ARTIKEL1' ). cl_aunit_assert=>assert_equals( EXP = 2 ACT = grossWeight MSG = '' ). grossWeight = materialFacade->getGrossWeight( 'EXISTING_ARTIKEL2' ). cl_aunit_assert=>assert_equals( EXP = 2 ACT = grossWeight MSG = '' ). grossWeight = materialFacade->getGrossWeight( 'MISSING_ARTIKEL' ). cl_aunit_assert=>assert_equals( EXP = 0 ACT = grossWeight MSG = '' ). endmethod. ENDCLASS.
To be more accurate in the description I moved out the standard mock classes which were only for comparision reason. You can find the description for this objects in version 4.