Skip to Content
Author's profile photo Uwe Fetzer

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.

Assigned Tags

      18 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Michelle Crapo
      Michelle Crapo

      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.

      Author's profile photo Bärbel Winkler
      Bärbel Winkler

      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

       

      Author's profile photo Justin Loranger
      Justin Loranger

      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".

      Author's profile photo Jens Knappik
      Jens Knappik

      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

      Author's profile photo Joao Sousa
      Joao Sousa

      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.

       

      Author's profile photo Fabian Lupa
      Fabian Lupa

      Like the OpenSQL and CDS Test Double frameworks?

      Author's profile photo Uwe Fetzer
      Uwe Fetzer
      Blog Post Author

      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

      Author's profile photo Andre Adam
      Andre Adam

       

      Why creating CDS views and not directly using OSQL test double framework?

      Author's profile photo Uwe Fetzer
      Uwe Fetzer
      Blog Post Author

      There is a "OSQL test double framework"? Do you have a link?

      Author's profile photo Andreas Göb
      Andreas Göb

      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.

       

       

      Author's profile photo Uwe Fetzer
      Uwe Fetzer
      Blog Post Author

      oh, yea, totally forgot this. Thank you!

      Here's the link to the documentation: here

      Author's profile photo Uwe Fetzer
      Uwe Fetzer
      Blog Post Author

      .. not available under 7.50, so it will never be possible to use the framework under ERP on premise 🙁

      Author's profile photo Joachim Rees
      Joachim Rees

      Hey Uwe,

      not available under 7.50, so it will never be possible to use the framework under ERP on premise

      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

      Author's profile photo Uwe Fetzer
      Uwe Fetzer
      Blog Post Author

      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)

      Author's profile photo Joachim Rees
      Joachim Rees

      ok, got it now, thanks for clarifying !

      Author's profile photo Uwe Fetzer
      Uwe Fetzer
      Blog Post Author

      There's a downgrade discussion on DSAG (German only, user needed): here

      Author's profile photo Andreas Göb
      Andreas Göb

      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/

      Author's profile photo Nigel James
      Nigel James

      Excellent Uwe. Thanks for the solid example.

      Nigel