Technical Articles
How to save any data type to DB and restore them.
Hello community,
this article describe solution to mock the data without need it to COPY + PASTE them.
In this approach you need to get the data in the preferred way (it can be SQL query, FUNCTION CALL and so one…), and save it to DB.
For example:
It can be some record from the table MARA. For this case you write one SQL query and save result to the Z-table. After you did it, you can always use the data from Z-table and DON’T NEED to mock MARA-data manually.
How should it works?
Two things, that we need: dynamic programming and serialization.
First thing first. We need a DB-Table to store our data. For this example we crate a table with two fields – MOCK_NAME and SERIALIZED. MOCK_NAME will be store the name of the mock object and SERIALIZED our serialized data.
Done.
First step – we define the interface for serializable objects.
INTERFACE zif_serializable_object
PUBLIC .
INTERFACES if_serializable_object.
METHODS set_data
IMPORTING i_data TYPE any.
METHODS read_mock_data
RETURNING VALUE(r_result) TYPE REF TO data.
METHODS how_is_my_name
RETURNING VALUE(r_result) TYPE zmock_name.
ENDINTERFACE.
Done.
To avoid the dependencies in the future we create also an interface for creator. The class that implement this interface can CRUD operations (Create, Read, Update, Delete)
INTERFACE zif_mock_creater
PUBLIC .
METHODS: create_mock_obj
IMPORTING i_mock_name TYPE zmock_name
RETURNING VALUE(r_result) TYPE REF TO zif_serializable_object.
METHODS: read_mock_obj
IMPORTING i_mock_name TYPE zmock_name
RETURNING VALUE(r_result) TYPE REF TO zif_serializable_object.
METHODS: update_mock_obj
IMPORTING i_mock TYPE REF TO zif_serializable_object
i_data TYPE any
RETURNING VALUE(r_result) TYPE REF TO zif_serializable_object.
METHODS: delete_mock_obj
IMPORTING i_mock_name TYPE zmock_name
RETURNING VALUE(r_result) TYPE sy-subrc.
ENDINTERFACE.
Now its time to implement serializable class.
CLASS zcl_mock DEFINITION
PUBLIC
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_serializable_object.
METHODS constructor
IMPORTING
i_mock_name TYPE zmock_name.
PROTECTED SECTION.
PRIVATE SECTION.
DATA: mock_name TYPE zmock_name,
mock_data TYPE REF TO data.
ENDCLASS.
CLASS zcl_mock IMPLEMENTATION.
METHOD constructor.
me->mock_name = i_mock_name.
ENDMETHOD.
METHOD zif_serializable_object~how_is_my_name.
r_result = mock_name.
ENDMETHOD.
METHOD zif_serializable_object~set_data.
DATA: type_name TYPE string.
DATA(type) = cl_abap_typedescr=>describe_by_data( i_data ).
CASE type->kind.
WHEN cl_abap_typedescr=>kind_intf.
WHEN cl_abap_typedescr=>kind_class.
WHEN cl_abap_typedescr=>kind_elem.
DATA(type_descr) = cl_abap_elemdescr=>describe_by_data( i_data ).
type_name = type_descr->get_relative_name( ).
CREATE DATA mock_data TYPE (type_name).
WHEN cl_abap_typedescr=>kind_struct.
DATA(struc_descr) = CAST cl_abap_structdescr( cl_abap_datadescr=>describe_by_data( i_data ) ).
type_name = struc_descr->get_relative_name( ).
CREATE DATA mock_data TYPE (type_name).
WHEN cl_abap_typedescr=>kind_table.
DATA(table_descr) = CAST cl_abap_tabledescr( cl_abap_tabledescr=>describe_by_data( i_data ) ).
DATA(line_decr) = table_descr->get_table_line_type( ).
type_name = line_decr->get_relative_name( ).
TYPES ty_sflight_lines TYPE STANDARD TABLE OF sflight WITH EMPTY KEY.
CREATE DATA mock_data TYPE ty_sflight_lines.
WHEN cl_abap_typedescr=>kind_ref.
WHEN OTHERS.
RETURN.
ENDCASE.
FIELD-SYMBOLS <dref> TYPE any.
ASSIGN me->mock_data->* TO <dref>.
<dref> = i_data.
ENDMETHOD.
METHOD zif_serializable_object~read_mock_data.
r_result = mock_data.
ENDMETHOD.
ENDCLASS.
How you can see this method implements zif_serializable_object interface. The most interesting method in this class is set _data(). There I check for the type of the input parameter and then assign it to mock_data attribute.
Last class in this chain is creator class.
CLASS zcl_mock_creator DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_mock_creater.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_mock_creator IMPLEMENTATION.
METHOD zif_mock_creater~create_mock_obj.
r_result = NEW zcl_mock( i_mock_name ).
ENDMETHOD.
METHOD zif_mock_creater~read_mock_obj.
TRY.
DATA(deserialized_obj) = NEW zcl_mock_serializer( )->deserialize( i_mock_name ).
CATCH zcx_mock_err INTO DATA(exc).
ENDTRY.
r_result = deserialized_obj.
ENDMETHOD.
METHOD zif_mock_creater~update_mock_obj.
i_mock->set_data( i_data ).
DATA(serialized_mock) = NEW zcl_mock_serializer( )->serialize( i_mock ).
DATA(ser_line) = VALUE zmock_serialized( mock_name = i_mock->how_is_my_name( )
serialized = serialized_mock ).
MODIFY zmock_serialized FROM ser_line.
COMMIT WORK.
ENDMETHOD.
METHOD zif_mock_creater~delete_mock_obj.
DELETE FROM zmock_serialized WHERE mock_name = i_mock_name.
r_result = sy-subrc.
ENDMETHOD.
ENDCLASS.
In this class we define the methods for Create, Read, Update and Delete functions.
ZCL_MOCK_SERIALIZER just a helper that serialize and deserialize the object. The date which use ZCL_MOCK_SERIALIZER has the XSTRING type. The same type we use in our DB-Table to save the object.
CLASS zcl_mock_serializer DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS serialize
IMPORTING i_object TYPE REF TO zif_serializable_object
RETURNING VALUE(r_result) TYPE xstring.
METHODS deserialize
IMPORTING i_mock_name TYPE zmock_name
RETURNING VALUE(r_result) TYPE REF TO zif_serializable_object
RAISING zcx_mock_err.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_mock_serializer IMPLEMENTATION.
METHOD serialize.
CALL TRANSFORMATION id SOURCE obj = i_object RESULT XML DATA(xstring)
OPTIONS data_refs = 'heap-or-create'.
r_result = xstring.
ENDMETHOD.
METHOD deserialize.
SELECT SINGLE serialized
FROM zmock_serialized
INTO @DATA(xstring)
WHERE mock_name = @i_mock_name.
IF xstring IS INITIAL.
RAISE EXCEPTION TYPE zcx_mock_err
EXPORTING
textid = zcx_mock_err=>dyn_mess
param1 = 'No serislized object was found.'.
ENDIF.
TRY.
DATA deserialized_object TYPE REF TO zif_serializable_object.
CALL TRANSFORMATION id SOURCE XML xstring RESULT obj = deserialized_object.
CATCH cx_xslt_runtime_error.
ROLLBACK WORK.
RETURN.
ENDTRY.
r_result = deserialized_object.
ENDMETHOD.
ENDCLASS.
Now is interesting part. So looks a test program:
We can choose any type to select and save the mock data.
REPORT.
FIELD-SYMBOLS <dref> TYPE any.
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE title.
SELECTION-SCREEN BEGIN OF LINE.
PARAMETERS: p_rb1 RADIOBUTTON GROUP grp1 DEFAULT 'X'.
SELECTION-SCREEN COMMENT (15) lbl1 FOR FIELD p_rb1.
PARAMETERS: p_rb2 RADIOBUTTON GROUP grp1.
SELECTION-SCREEN COMMENT (15) lbl2 FOR FIELD p_rb2.
PARAMETERS: p_rb3 RADIOBUTTON GROUP grp1.
SELECTION-SCREEN COMMENT (15) lbl3 FOR FIELD p_rb3.
SELECTION-SCREEN END OF LINE.
SELECTION-SCREEN END OF BLOCK b1.
INITIALIZATION.
title = 'Datentyp auswΓ€hlen'.
lbl1 = 'Integer:'.
lbl2 = 'Structure:'.
lbl3 = 'Table:'.
START-OF-SELECTION.
DATA(mock_builder) = NEW zcl_mock_creater( ).
* Any data type
CASE 'X'.
WHEN p_rb1.
DATA numb TYPE i VALUE 5.
WHEN p_rb2.
SELECT SINGLE * FROM MARA INTO @DATA(mara_record).
WHEN p_rb3.
SELECT * FROM sflight UP TO 10 ROWS INTO TABLE @DATA(sflight_tab).
ENDCASE.
* 1 Create
DATA(mock) = mock_builder->zif_mock_creater~create_mock_obj( 'MY_MOCK_OBJECT' ).
* 2 Update
CASE 'X'.
WHEN p_rb1.
mock_builder->zif_mock_creater~update_mock_obj( i_mock = mock i_data = numb ).
WHEN p_rb2.
mock_builder->zif_mock_creater~update_mock_obj( i_mock = mock i_data = mara_record ).
WHEN p_rb3.
mock_builder->zif_mock_creater~update_mock_obj( i_mock = mock i_data = sflight_tab ).
ENDCASE.
* 3 Read de-serialized from DB
DATA(mock_data) = mock_builder->zif_mock_creater~read_mock_obj( 'MY_MOCK_OBJECT' )->read_mock_data( ).
ASSIGN mock_data->* TO <dref>.
* 4 Delete
mock_builder->zif_mock_creater~delete_mock_obj( 'MY_MOCK_OBJECT' ).
If you start the program, you can see that is no matter which data we want to save into DB. After read data from DB the helper class serialize the data and it will be saved into DB. After that we can read the data and get the same format of the data that we save into Z-table (MARA-Record for example). Is it not a great?! π
I hope this was understandable.
In higher releases SAP supplies class CL_OSQL_REPLACE which tricks actually SQL statements into reading from a Z table during unit tests. However I am not sure that is a good idea. I am pretty much against doing any sort of database access in unit tests. The "code pal" code inspector tests give you an error if it finds that sort of thing.
One reason is that database access slows down the tests and you really need to be able to execute hundreds of tests in seconds.
Also as soon as you have a Z table a test reads from, then it is possible (unlikely but possible) that the data in the Z table changes over time, leading to tests that once passed now not passing, or vice versa despite the test being unchanged.
I do agree that cutting and pasting vast amounts of data is a pain. I always point people to
https://github.com/sbcgua/mockup_loader
In this case the data is read from an excel sheet stored inside the database. Of course you might say that is exactly the same as the method you describe....
Thank you for the comment and explanation. I'm absolute agree with this points and I will not recommend to use this approach for unit-test mocking.
.
Mykola, thank you for sharing you vision for mock-data.
I have noticed that you missed realization for ref-objects. Why?
I mean class ZCL_MOCK, method set_data
Sometime ago I have using the similar approach for testing and it DOES SPEED UP testing (does not matter what is saying a man in a hat
even if man in a hat named Paul Hardy
I am not intend to advertise his book here... ).
If it is ok - I could also share approach what I was using.
But I prefer not to call it unit-testing (actually it is not unit), but model-testing, which I think we need most from ABAP and SAP ERP itself. But it is only my opinion.
Hey Oleg,
thank you for the comment.
In this blog I just want to show an approach to serialize and de-serialize the data. It's not perfect and fully finished solution yet
Also some places with possible exceptions are not covered. Interesting for me is to get an opinion from sap-community.
Sure the place with
should be implemented
And sure I would be glad to see your approach what you using!
Β
Β
I think we have a difference of semantics here.
Having a large amount of test cases which can run in an automatic fashion coming from some sort of data source is certainly faster than manually typing in the same amount of data into unit test code.
One example I know of is a company who does mapping software. They have tens of thousands of addresses they check after every change to make sure the software change as not "moved" 10 Downing Street to Africa or somewhere.
It is the same with Postman - when we change something on the back or front end hundreds if not thousands of automated Postman calls are made to make sure nothing has been broken.
But as you say those are not unit tests - they are more like ECATT. In other words you read and write to/from the database. Sometimes tests like that can take hours to run. That does not matter as you are only interested in the results and can do other things in the meantime.
A unit test should have no database access because you want to run hundreds or thousands of them in a second, because you run them all the time whilst you are programming, maybe every time you change a line of code (you can set this up in ADT by the way with the continuous integration plug in). It is like the pretty printer or the syntax check. Both of those are instant and so can run after every single change - but if they took a long time you would not run them until maybe the end of the day, and so the time taken to detect errors would increase.
Also thank you for not advertising my book. It would be really bad to mention I have a new edition of my book out, so I also am not going to mention that there is a new edition of my book out. I certainly would not use this opportunity to direct people to my log about it
https://blogs.sap.com/2021/11/23/i-can-see-abap-moon-rising-part-two-of-eight/
Internally we have a similar solution which stores data in eCatt test data containers (object type ECTD).
The containers have the nested structure
for access
We use this as
So a container can have multiple tested objects, and each object has multiple input/output pairs. As long as you keep everything in json/xml this is a general-purpose solution.
Note that you can work with typed data structures in ECTD objects but if you have deeper structures you will go insane if you try to edit them manually. So we use this as a poor man's document store where you can copy-paste some json/xml if needed.
The data can be imported/exported manually, or with abapgit as this is a development object with a package assignment. The serialization format is not friendly as the whole container is saved as a singel xml file, but you do get the ability to revert to earlier versions.
This just takes care of the data storage, so to execute the tests, there are two ways:
As a lot of this is independent of what you are testing, if you're willing to publish your interfaces in a github repository, I could contribute some example implementations...