Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
UweFetzer_se38
Active Contributor
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.perfiljeva2 and c436ae948d684935a91fce8b976e5aa7 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.
18 Comments