Test Seams: Mocking von Code Abhängigkeiten in ABAP Unit

ABAP Unit ist unter SAP Entwicklern wohl bekannt. In der Praxis wird ABAP Unit jedoch wenig eingesetzt. Ein häufiger Grund ist, dass der zu testende Code starke Abhängigkeiten zu Datenkonstellationen hat. Wenn man nun nicht die Möglichkeit hat, den Code für eine bessere Testbarkeit umzubauen, kann man Test Seams verwenden. Test Seams erlauben es, Quelltext im produktiven Teil des Programms auszutauschen. Das ermöglicht ein Mocking der Datenabhängigkeiten.

Systemvoraussetzungen für Test Seams

Um Test Seams nutzen zu können, benötigt man mindestens einen SAP Netweaver in der Version 7.5 oder aufwärts. Die Verwendung beschränkt sich auf Klassen und Funktionsgruppen.

Grundlagen der Test Seams

Test Seams bieten die Möglichkeit im produktiven Code Test-Code bzw. ein bestimmtes Verhalten zu injizieren. Das bedeutet, dass das System anstatt des produktiven Codes ein Test Seam ausführt. Das gilt nur, wenn man einen Unit-Test ausführt. Über einen normalen Aufruf über bspw. ein Programm wird der produktive Code ausgeführt.

Anwendung der Test Seams

Ein Test Seam umschließt den Code wie folgt.

TEST-SEAM [Test-Seam-Name].
*     produktiver Code
END-TEST-SEAM.

Bis jetzt der zu injizierende Code nicht bekannt, weshalb man eine Test Injection in der zugehörigen Unit-Test-Klasse definiert. Eine Test Injection ist der Code, welcher im Unit-Test anstatt des produktiven Codes ausgeführt wird. Allgemein definiert man eine Test Injection mit dem nachfolgenden Block.

TEST-INJECTION [Test-Seam-Name].
*    Injizierter Code
END-TEST-INJECTION.

Alle innerhalb des Test Seams sichtbaren Variablen sind innerhalb der Test Injection zugreifbar. In einer Test Injection kann man demzufolge also nicht auf die Variablen in der ABAP Unit Testklasse zugreifen.

Beispiele für Test Seams

Für die Beispiele habe ich die Klasse ZCL_MK_TEST_SEAM implementiert, welche nachfolgende Methoden und lokale Klassen beinhaltet. Die globale und lokale ABAP Unit Testklasse stehen als Download zur Verfügung. Die Logik setzt auf dem EPM Datenmodell (Tabellen SNWD_*) auf.

Test-Seams: Klassenimplementierung

Beispiel 1: Berechnung des Durchschnittsbruttobetrags von Kundenaufträgen

Berechnungen stellen häufig ein Problem hinsichtlich des Testens dar. Dabei ist die Berechnung selbst nicht das Problem, sondern die Datensätze, auf denen die Berechnung basiert. Hier eignen sich Test Seams hervorragend zum Mocken von Daten. Das bedeutet, dass man keine Datensätze aus der Datenbank lädt, sondern fiktive Datensätze bildet, um die Funktionalität zu testen. Hierzu ein kurzes Beispiel:

Die Methode GET_SO_AVERAGE gibt den Durchschnittswert der Bruttowerte der Kundenaufträge zurück. Die Methode enthält zwei weitere Funktionen: Die Selektion der Daten und die Berechnung selbst.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_MK_TEST_SEAM->GET_SO_AVERAGE
* +---------------------------------------------------------------------------------------------+
* | [<-()] RV_SUM_AMOUNT                  TYPE        SNWD_TTL_GROSS_AMOUNT
* +--------------------------------------------------------------------------------------
  METHOD GET_SO_AVERAGE.

    select_data( ).

    rv_sum_amount = calculate_so_average( ).

  ENDMETHOD.    

Der Fokus der Test Seams ist die Methode SELECT_DATA. Im produktiven Code ist es notwendig, dass alle Datensätze aus der Datenbank gelesen werden. Das bedeutet, dass der Durchschnittswert variabel zu den Daten der Datenbank ist. Deshalb setze ich hier das Test Seam average_selection ein.

    TEST-SEAM average_selection.
      SELECT * FROM snwd_so INTO TABLE mt_so_headers.
    END-TEST-SEAM.

Die Injection average_selection beschreibt die Datensätze, welche man mockt. Hierbei erzeuge ich bei einem Unit-Test drei Datensätze, welche immer dieselben Werte besitzen.
Mit der Methode CALCULATE_SO_AVERAGE wird der Durchschnitt der Brutto-Beträge berechnet.

    FIELD-SYMBOLS <fs_snwd_so> TYPE snwd_so.

    LOOP AT mt_so_headers ASSIGNING <fs_snwd_so>.
      rv_calculated_average = ( rv_calculated_average + <fs_snwd_so>-gross_amount ).
    ENDLOOP.

    rv_calculated_average = rv_calculated_average  / lines( mt_so_headers ).

Basierend auf der vorherigen Implementierung habe ich die Testmethode TEST_GET_SO_AVERAGE implementiert. Ich rufe die Methode GET_SO_AVERAGE auf und überprüfe anschließend, ob die Funktionalität richtig mit den gemockten Daten gerechnet hat.

*   Code injizieren
    TEST-INJECTION average_selection.
      FIELD-SYMBOLS <fs_snwd_so> TYPE snwd_so.

      APPEND INITIAL LINE TO mt_so_headers ASSIGNING <fs_snwd_so>.
      <fs_snwd_so>-gross_amount = 20.

      APPEND INITIAL LINE TO mt_so_headers ASSIGNING <fs_snwd_so>.
      <fs_snwd_so>-gross_amount = 50.

      APPEND INITIAL LINE TO mt_so_headers ASSIGNING <fs_snwd_so>.
      <fs_snwd_so>-gross_amount = 50.

    END-TEST-INJECTION.

*   Zu testenden Code ausführen
    DATA(lv_calculated_average) = mr_test_seam->get_so_average( ).

    cl_abap_unit_assert=>assert_equals(
      EXPORTING
        act                  = lv_calculated_average
        exp                  = 40  ).

Wenn die Funktionalität richtig rechnet, wird der Test erfolgreich sein.

Beispiel 2: Berechtigungsprüfung auf Business-Partner-ID

Berechtigungsprüfungen stellen häufig ein Problem für Unit-Tests dar, da der ausführende Benutzer nicht über die Berechtigung verfügt, die Funktionalität auszuführen. Hier finden Test Seams einen weiteren Anwendungsbereich. Nachfolgend findest du dafür ein Beispiel.

Die Methode HAS_AUTHORITY überprüft, ob der Benutzer für das Feld BP_ID der EPM-Business-Partner  Berechtigungen besitzt. Falls das der Fall ist, besitzt die Methode den Rückgabewert abap_true, andernfalls abap_false. Diese Überprüfung ist mit dem Test Seam authority umgeben. Bei der Ausführung des dazugehörigen Unit-Tests wird das Test Seam authority injiziert.

* ---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_MK_TEST_SEAM->HAS_AUTHORITY
* +---------------------------------------------------------------------------------------------+
* | [--->] IV_PARTNER_ID                  TYPE        SNWD_PARTNER_ID
* | [<-()] RV_HAS_AUTHORITY               TYPE        ABAP_BOOL
* +--------------------------------------------------------------------------------------
  METHOD has_authority.

    TEST-SEAM authority.

      AUTHORITY-CHECK OBJECT 'S_EPM_BP'
       ID 'EPM_BP_ID' FIELD iv_partner_id.
      IF sy-subrc = 0.
        rv_has_authority = abap_true.
      ELSE.
        rv_has_authority = abap_false.
      ENDIF.

    END-TEST-SEAM.

  ENDMETHOD.

Es ist nicht sichergestellt, dass der aufrufende Benutzer des Unit-Tests auch wirklich die Berechtigung für das Feld BP_ID besitzt, führt man die Injection authority aus.
Da ich Berechtigungen auf das EPM-Modell habe, setze ich den Wert in der Test Injection auf abap_false. In anderen Fällen könnte ich damit überprüfen, ob nur für ganz bestimmte Benutzer bspw. eine Transaktion aufgerufen werden kann.

Die Testmethode TEST_AUTHORITY_CHECK ruft die ursprünglich implementierte Funktionalität auf und vergleicht die Ergebnisse. Da das Test Seam den Wert abap_false für has_authority injiziert, wird der Test erfolgreich sein.

*   Code injizieren
    TEST-INJECTION authority.
      rv_has_authority = abap_false.
    END-TEST-INJECTION.

*   Zu testenden Code ausführen
    DATA(lv_has_authority) = mr_test_seam->has_authority( '4711' ).

    cl_abap_unit_assert=>assert_equals(
      EXPORTING
        act                  = lv_has_authority
        exp                  = abap_false ).

Fazit

Test Seams ermöglichen das Injizieren von Testcode im Rahmen von ABAP Unit. Dadurch kann man bspw. Datenabfragen mocken, Berechtigungen umgehen oder Berechnungen überprüfen. Insbesondere bei Anwendungen, die über Jahre hin gewachsen sind und der Code viele Abhängigkeiten besitzt, zeigen Test Seams ihre Stärken.

Ein alternativer Ansatz ist das Test Double-Framework von SAP. Allerdings können dabei nur Interfaces verwendet werden und nicht Klassen oder Funktionsgruppen, bei denen man direkt im Code die zu testenden Stellen kennzeichnet. Zudem ist es viel schwieriger, gegen die API des Test Double-Frameworks zu implementieren.

Insgesamt finde ich, dass sie eine richtig nützliche und pragmatische Erweiterung für Unit-Tests sind.

 

Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion.

 

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Veröffentlicht in ABAP

Über den Autor

Michael Krause

Kommentar verfassen