ABAP Unit Tests: real world example for Jelena and Michelle
In the discussion under the blog post “ABAP – The Special Snowflake” by Nigel James Jelena and Michelle asked me for a blog post about a real world example for ABAP Unit tests.
As you may know, you can’t (shouldn’t) reject wishes from Jelena Perfiljeva and Michelle Crapo so here we go:
Background: Public services / Utilities
Mission: write a gateway service to get prices for meter readings for different net providers (direct via net ID or customer address)
(I’ll just show parts of the data retrieval class here, not the whole GW service)
In my case I’m using a simple type of Unit Tests, the “Constructor Injection”, that’s why we need three classes:
- the data retrieval class which also contains the test class
- the database class for database selections and external dependences like function module calls
- the data mocking class / test double
In your data retrieval class you start coding like you would normaly do, for example
METHOD get_price.
DATA(l_priceid) = get_priceid(
i_price = i_price
i_preistuf = i_preistuf
).
CHECK l_priceid IS NOT INITIAL.
r_result = get_priceamount(
i_netprovider = i_netprovider
i_priceid = l_priceid
).
ENDMETHOD.
In the “get_priceid” method you would normaly place your select statement. In our case we call a method in the database class instead:
METHOD get_preisid.
r_result = reading_db->get_priceid(
i_price = i_price
i_preistuf = i_preistuf
).
ENDMETHOD.
Every database access, function module or external method call you have encapsulate like this.
The methods in the database class would look like this (as you can see, you first have to create an interface, we will need this later for the test double class):
METHOD zif_icos_reading_db~get_priceid.
SELECT SINGLE nb_preisid
INTO r_result
FROM /cronos/nb_preis
WHERE preis = i_price
AND preistuf = i_preistuf.
ENDMETHOD.
METHOD zif_icos_reading_db~get_price_amount.
CALL FUNCTION '/CRONOS/NB_GET_PRICE_AMOUNT'
EXPORTING
x_stichtag = sy-datum " Stichtag
x_preisid = i_priceid " Netbill Preisschlüssel ID
x_netznr = i_netprovider-netznr " Eindeutige Nummer des Netzgebietes
x_messgebiet_msb = i_netprovider-messgebiet_msb " Messgebiet des Standardmessstellenbetreibers
x_messgebiet_mdl = i_netprovider-messgebiet_mdl " Messgebiet des Standardmessdienstleisters
x_messgebiet_msys = i_netprovider-messgebiet_imsys " Messgebiet des Standard-MSB für intelligente Messsysteme
x_spebeneid = '1' " Internes Kennzeichen der Spannungsebene
TABLES
xy_epreih = r_result " Historientabelle für alle Preise
EXCEPTIONS
general_fault = 1
no_preisid = 2
no_net = 3
no_spebene = 4
OTHERS = 5.
ENDMETHOD.
If you instanciate the database class in the constructor of the data retrieval class your code should work already.
Because in our unit tests we only want to test our code and not the external dependencies like selects or function modules, we can simulate theses in a so called “Test Double” class:
In the test double we implement the same interface like we have used in the database class. As a result we have to implement the same methods, but with simulated data:
METHOD zif_icos_reading_db~get_priceid.
r_result = COND #(
WHEN i_price = 'ZANDET1ZZZ' AND i_preistuf = '~NMSB' THEN '11753'
WHEN i_price = 'ZELMMT1MME' AND i_preistuf = '~MSB_Z25' THEN '18007'
WHEN i_price = 'ZELMMT1MME' AND i_preistuf = '~MSB_Z20' THEN '18013'
WHEN i_price = 'ZELMMT1MME' AND i_preistuf = '~MSB_Z18' THEN '18014'
).
ENDMETHOD.
METHOD zif_icos_reading_db~get_priceamount.
r_result = VALUE #(
( priceamount = SWITCH #(
i_preisid
WHEN '11753' THEN '8.00'
WHEN '18007' THEN '16.00'
WHEN '18013' THEN '80.00'
WHEN '18014' THEN '110.00'
ELSE '0.0'
)
)
).
ENDMETHOD.
Back to the data retrieval class. Now we have two alternative classes which can return data, one direct from the database/function modules and one with simulated data.
Which one do we want to use and how do we do this? Here we can use the so called “Constructor Injection”, where we can switch the classes. In the regular process (for example called by the gateway service), we simply instanciate the database class. If we are called by the Unit test, we use the instance of the test double class which we’ll instanciate in the setup method of the test class (see below).
The constructor of the data retrieval class would look like this:
METHOD constructor.
reading_db = COND #( WHEN i_reading_db IS BOUND "called by unit test
THEN i_reading_db "-> use this instance (the test double)
ELSE NEW zcl_icos_reading_db( ) "called by gateway -> create database access instance
).
ENDMETHOD.
In the setup method of the test class we instanciate the test double class and pass this instance to the constructor of our data retrieval class:
METHOD setup.
cut = NEW zcl_icos_reading( NEW zcl_icos_reading_db_double( ) ).
ENDMETHOD.
Now we can create our test methods in our test class, for example
METHOD messpreise_by_wrong_address.
TRY.
DATA(lt_readingprices) = cut->get_readingprice_by_address(
i_strasse = 'BlaBla'
i_hausnummer = '2'
i_pstlz = '55555'
i_ort = 'Köln'
).
cl_abap_unit_assert=>fail( msg = 'Wrong address should raise an error' ).
CATCH zcx_icos_meter ##no_handler.
ENDTRY.
ENDMETHOD.
@Jelena and Michelle : I hope this helps a little bit on how to use Unit Tests in the real world.
P.S.: and here’s an example for my “Islands of happiness”:
FORM very_old_code.
some_code.
more_old_code.
new ZCL_MY_ISLAND_OF_HAPPINESS( )->my_new_method_already_tested_with_unit_tests( ).
even_older_code.
ENDFORM.
Awesome Blog!!!! Real world. And it even makes sense to me. Big bonus.
Thank you for the Island of Happiness. 🙂 I've been tackling old code lately.
Michelle
BTW - You can happily ignore my suggestions. My 20 year old son ignores me when he wants. However, I'm so glad you didn't.
Disregarding the "for Jelena and Michelle" part of the title I read the blog post as well ? and will refer back to it when and as needed. So thanks from me, Uwe, for taking the time to provide this example for folks like Jelena, Michelle and myself! I hope that others follow your example and post similar real life code snippets for ABAP unit tests (and thereby ABAP OO) we can learn from.
Cheers
Baerbel
Thank you Uwe, very good and simple examples.
I have been doing more of my own trial and error coding with my own "islands of happiness".
Hey Uwe,
nice blog! Please tell me where your island of happiness is!!!
I will have method names with more or equal 44 characters, too!!!
*Pack the bag* 😉
Cheers
Jens
The SELECTs are also “our code”, code that needs to run fine, therefore needs to be tested.
That’s why this is a half measure approach to unit testing. SAP should really provide a test runtime as other softwares do, where the DB is clear for each test run.
It means you have to include data load in the test but it makes the test repeatable.
Like the OpenSQL and CDS Test Double frameworks?
I think simple selects like I've used in my example we don't have to test. For more complex ones you can create CDS views which can be tested separately with the CDS Test Double framework
Why creating CDS views and not directly using OSQL test double framework?
There is a "OSQL test double framework"? Do you have a link?
The only reference I know of and I could find is on open.sap.com in the “Writing Testable Code for ABAP” course, Week 6, Unit 2, Slide 3. Not sure whether I can freely share it here.
Basically, with ABAP 7.50, it says you can define an internal table in your test class with the type standard table of your_db_table, and then use g_environment = cl_osql_test_environment=>create( i_dependency_list_entity = VALUE #( ( ‘your_db_table’ ) ) to tell the framework to re-route all OpenSQL queries to the test double.
You can insert test data into the test double in your tests using g_environment->insert_test_data( your_itab ). Combine that with calls to g_environment->clear_doubles( )in the test setup and g_environment->destroy( )in the teardown, and you should get pretty far.
Unfortunately, I cannot use it in my own work, because we need to stay compatible with earlier ABAP versions.
oh, yea, totally forgot this. Thank you!
Here's the link to the documentation: here
.. not available under 7.50, so it will never be possible to use the framework under ERP on premise 🙁
Hey Uwe,
I’m not sure if I get your point here: once you go to EHP8 (for SAP ECC , SAP_ APPL 618), you will also have NW7.50 (SAP_BASIS 750 ).
This is availabel on premise right now. (And we have customers going there).
best
Joachim
I mean the class cl_osql_test_environment is not available in 7.50. (and not in 7.51 by the way, I think you need >= 7.52)
ok, got it now, thanks for clarifying !
There's a downgrade discussion on DSAG (German only, user needed): here
Another mention of this is provided here: https://blogs.sap.com/2018/04/21/sap-open-course-unit-testing-week-6-working-with-existing-code/
Excellent Uwe. Thanks for the solid example.
Nigel