Skip to Content

Introduction

This week, I faced a situation where I needed to implement a new business object for one of our applications. As business objects usually need persistence, I created a new repository for it to handle the database access. This repository covered the standard database functions like Create, Update, Read and Delete (CRUD).

While implementing the repository, the question of what should happen to new business objects during the Create-operation came up, specifically who should create a new document number. I decided to request new number ranges for new business objects while in the SAVE-Method of the repository and then the trouble started to grow…

Bad design

The first design was a mess:

v1.PNG

In this fictitious example, ZCL_BOOKING represents an entity, a booking record, which will be saved to the database.

ZCL_BOOKING_REPOSITORY takes care of saving this entity correctly, with some more functions like retrieving those entities back from database or delete them from the DB.

The SAVE-Method looked like this:

CLASS ZCL_BOOKINGS_REPOSITORY IMPLEMENTATION.
...
METHOD ZIF_BOOKING_REPOSITORY~save.
  DATA lv_bookid TYPE S_BOOK_ID.
  DATA ls_header TYPE bookings.
  lv_bookid = io_booking->get_bookid( ).
  ls_header = io_booking->get_booking( ).
  IF lv_bookid IS INITIAL.
    CALL FUNCTION 'NUMBER_GET_NEXT'
      EXPORTING
 nr_range_nr             = 'ZFL_BOOKID'
 object                  = '01'
      IMPORTING
 number                  = rv_bookid
      EXCEPTIONS
 interval_not_found      = 1
 number_range_not_intern = 2
 object_not_found        = 3
 quantity_is_0           = 4
 quantity_is_not_1       = 5
 interval_overflow       = 6
 buffer_overflow         = 7
 OTHERS                  = 8.
*   implement proper error handling here...
 io_booking->set_bookid( lv_bookid ).
    CALL FUNCTION 'Z_FM_INSERT_BOOKINGS' IN UPDATE TASK
      EXPORTING
 iv_bookid                                 = lv_bookid
 is_header             = ls_header.
  ELSE.
    CALL FUNCTION 'Z_FM_UPDATE_BOOKINGS' IN UPDATE TASK
      EXPORTING
 iv_bookid                                 = lv_bookid
 is_header             = ls_header.
  ENDIF.
ENDMETHOD.
...
ENDCLASS.

Running out of time? I’M RUNNING OUT OF NUMBERS!!!

Everything went well, until I started to write a unit test for the SAVE-method.

I started to write a test which created a new instance of type ZCL_BOOKING without any BOOKID. I expected the instance to be inserted to the database. This worked pretty well, but when I tried to implement the TEARDOWN method I had some issues. As Unit tests need to be processed as often as you need, and should leave the system in the same state from where you started from. This means, the inserted record needed to be deleted.

As the SAVE-method requests a new number for each object that has not yet an ID, I don’t really know which ID has just been inserted.

I could have asked the instance of type ZCL_BOOKING which ID has been set for it. This would solve at least the issue that I need to clean up the database after the test-insert.

But, more severe, the current number in the number range interval has increased by one with each unit test. This was not acceptable.

So the unit test revealed the bad design: In fact, the repository had a dependency on a number range object. Actually, it should not care about it.

Refactored design

This step introduces a new class to the design, which is called ZCL_NUMBER_RANGE_REQUEST. It implements an interface ZIF_NUMBER_RANGE_REQUEST which is now used by ZCL_BOOKING_REPOSITORY to handle its number range requests.

  The number range object is created before ZCL_BOOKING_REPOSITORY is getting created in order to hand it over to the constructor of the repository.

v2.PNG

The result is: Instead of creating new document numbers by its own, the repository asks another object for it.

This has a huge benefit: As the number range object is specified by an interface, we can fake this interface in a unit test and pass it to the repository’s constructor. The fake object of course does not request a real number from a real number range but returns “1” all the time.

Unit Test Setup.png

So that’s what the new implementation of the classes looks like:

CLASS ZCL_BOOKING_REPOSITORY IMPLEMENTATION.
...
METHOD ZIF_BOOKING_REPOSITORY~save.
  DATA lv_bookid TYPE S_BOOK_ID.
  DATA ls_header TYPE bookings.
  lv_bookid = io_booking->get_bookid( ).
  ls_header = io_booking->get_booking( ).
  IF lv_bookid IS INITIAL.
    lv_bookid = mo_number_range->get_next_number( ).
    io_booking->set_bookid( lv_bookid ).
    CALL FUNCTION 'Z_FM_INSERT_BOOKINGS' IN UPDATE TASK
      EXPORTING
 iv_bookid                                 = lv_bookid
 is_header       = ls_header.
  ELSE.
    CALL FUNCTION 'Z_FM_UPDATE_BOOKINGS' IN UPDATE TASK
      EXPORTING
 iv_bookid                                 = lv_bookid
 is_header       = ls_header.
  ENDIF.
ENDMETHOD.
METHOD constructor.
 mo_number_range = io_number_range.
ENDMETHOD.
ENDCLASS.

The implementation of the number range object requests the current number by using the standard function module. The input paramaters for this function module have been provided in the CONSTRUCTOR of the object.

CLASS ZCL_NUMBER_RANGE_REQUEST IMPLEMENTATION.
...
method GET_NEXT_NUMBER.
    CALL FUNCTION 'NUMBER_GET_NEXT'
      EXPORTING
 nr_range_nr             = mv_nrobj
 object                  = mv_nrnr
      IMPORTING
 number                  = rv_number
      EXCEPTIONS
 interval_not_found      = 1
 number_range_not_intern = 2
 object_not_found        = 3
 quantity_is_0           = 4
 quantity_is_not_1       = 5
 interval_overflow       = 6
 buffer_overflow         = 7
 OTHERS                  = 8.
*   implement proper error handling here...
endmethod.
ENDCLASS.

How can I really get a fake?

Fake objects can be created using local classes in the unit test. As an alternative, mocking Frameworks can help to automate this task by providing a declarative API.

In the real life use case I created the fake object using a mocking framework with this call:

    mo_number_range_request ?= /leos/cl_mocker=>/leos/if_mocker~mock( ‘/leosb/if_number_range_request’)->method( 'GET_NEXT_NUMBER' )->returns( 1 )->generate_mockup( ).

I hate puzzles

This kind of architecture eventually leads to tons of classes, each having their own responsibility. In a real life application you would need to set up the repository instances and their dependencies at least in some specific method at startup:

CREATE OBJECT lo_number_range TYPE ZCL_NUMBER_RANGE_REQUEST
CREATE OBJECT lo_repository TYPE ZCL_BOOKING_REPOSITORY EXPORTING io_number_range = lo_number_range.

IoC Containers help you in managing these dependencies by allowing you to register specific classes for each interface in customizing. Their purpose is to resolve all dependencies for a root object, when a root object like a repository is requested by the application. The container creates this root object and hands it back to the caller with just one single line of code.

Related links

http://scn.sap.com/community/abap/application-development/objects/blog/2012/11/16/unit-tests-in-abap-using-a-mocking-framework

http://scn.sap.com/community/abap/application-development/objects/blog/2012/11/20/ioc-container-in-abap-part-i

http://scn.sap.com/community/abap/application-development/objects/blog/2012/11/28/ioc-container-in-abap-part-ii

To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

  1. Dirk Wittenberg

    Great post!

    Lucky one who has a mocking framework! Did you / your company develope it ?

    I’m always puzzling with factory classes for the productive classes and another one for the fakes.

    Kind regards,

    Dirk

    (0) 
    1. Uwe Kunath Post author

      Dear Dirk, yes we developed a mocking library/framework based on our needs in unit testing. As of now it supports mocking based on interfaces and non-final, concrete classes with at least protected constructors.

      Your comment on factory classes sounds interesting to me. Usually, when I test entity objects, there is no need to use factories, or if so, I just mock them also.

      Instead, while I test factories, I usually use mocked entities to make the test independent of the entity’s implementation details. This means, everything can be achieved with local classes, no need for specific global classes for fake purposes.

      Regards,

      Uwe

      (0) 

Leave a Reply