Mocking DAOs with ABAP Test Double Framework
Introduction
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.
Approach
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.
Implementation
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.
Hi Andreas,
If you are on Release > ABAP 740 (SP09) you can also make use of the standard SAP Mocking-Framework. ABAP Test Double Framework - An Introduction
BR,
Suhas
PS - Unfortunately the std. framework supports mocking of interfaces only
Hi Suhas!
In the example code I am using the new ABAP test double framework. I packed the creation of the test double in a separate class ZCL_DAO_MATERIAL_TEST_DOUBLE (appendix 4) thus I have not to implement and configure the test double for each unit test.
The relevant code regarding to the creation of the ABAP test double in this example is:
The injection is then done in each unit test , eg. ZCL_DAO_MATERIAL_FACADE_TDOUB_UNIT (appendix 7)
The mocking example in the end of the document is only for comparison reason, maybe I should highlight this more in the document.
best regards, Andreas
Hi Andreas,
thanks for this insight in the double test framework!
I wonder if it is really necessary to implement such complex test doubles...
In this case you implement a lot of lines of code just for one single value.
I understand the idea behind it and of course this is only a show case to make me understand how this works.
IMHO with this approach it is not possible to handle more complex data, like tables or objects.
When doing unit testing you must design the methods in a way that unit testing is possible. For DAO you also need an interface which might not exist for an already existing class you have to use to get your data.
so: Why do not design in a way that needed data can be inserted at the beginning of a unit test instead of calling a SELECT_DATA method?
Like
Especially if there is not only ONE characteristic of data you need (e.g. one material number), but different "configurations"? I think about a sales document which consists of VBAK, VBKD, VBAP, VBEP, VBPA.
I find it really hard to get along with all test cases that are needed. If I also have to program and to maintain and check dependend data... :/
Cheers
~ Enno
Hi Enno!
First of all the blog is some years old. The capabilities in ABAP evolved and mine too 🙂
I remember exactly when I started to write this blog which was when I had to make a critial change in an BADI and I wanted to test not only the added logic but also if the BADI was working correct (by "injecting" the productive logic or the test double logic into the BADI implemementation.
Well I am sure there is an easier way to do it and of course good implementations lead to unit tests without mocks.
... and one day (on ABAP version 7.52) or higher all the mocking above is really deprecated , as then it will be done with the Open SQL test double framework - an incredible promising and powerful mocking tool in my point of view.
I Bet they did! 🙂
But the Technique itself is still in use, isn't It?
Never heard about this paradigm before... Can you explain why?
Looks as if I could insert my testdata into a kind of "test layer" where the SELECT statement takes the data from?
"good implementations lead to unit tests without mocks"
Well its somehow an extreme paradigm which is of course not suitable in every situation but in some situations its certainly worth to remember it. And of course its only valid if the tests are distinguised between unit tests, functional tests and integration tests.
Here for example an article about it:
https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a
"OSQL" - I think yes some kind of "test layer" also meets my understanding of this framework.
Open SQL Test Double Framework : it works for any Open SQL, not only SELECT. From 7.52. You choose which tables are to be mocked, inject false data, and subsequent Open SQL based on this table will use this false data. So simple and so powerful.
And also ABAP CDS Test Double Framework ! From 7.51.