Modernes ABAP

ABAP Unit

(C) Brandeis Consulting.

Unit Tests

  • Unit Tests testen kleine Einheiten, normalerweise einzelne Methoden
  • Nach jeder Änderung am Code sollen die Tests durchgeführt werden
  • Die Tests lassen sich automatisieren, z.B. durch einen vollständigen nächtlichen Testlauf
  • Die Tests sollen nach Möglichkeit ohne Abhängigkeiten zu anderen Komponenten ablaufen
  • Die Clean Code Prinzipien erleichtern die Erstellung von UnitTests. Auch der Test Code wird nach diesen Prinzipien erstellt
(C) Brandeis Consulting.

Code Under Test (CUT)

Mit dem Kürzel CUT bezeichnet man das zu testenden Objekt. Diese Abkürzung heißt, je nach Kontext entweder Code under Test oder Class under Test oder sogar CDS under Test.

Diese Abkürzung wird auch häufig in Testklassen verwendet.

(C) Brandeis Consulting.

Testklassen

UnitTests werden in lokalen Klassen einer globalen Klasse implementiert. In der Definition haben sie den Zusatz FOR TESTING . Durch die Tastenkombination
Strg. + Shift + F10
werden alle Tests einer Klasse ausgeführt.

(C) Brandeis Consulting.

Laufzeit und Risikostufe - Empfehlungen

Laufzeit

Wir legen nach Möglichkeit schnelle Tests an, d.h. solche die deutlich weniger als eine Sekunde dauern. Damit ist die Durchführung jederzeit ohne Warten möglich.

Risikostufe

Unsere Tests verändern Persistente Daten nicht. Wenn wir Logik gegen Datenbankzustände testen wollen, verwenden wir das Test Double Framework.

Falls wir von den obigen beiden Punkten abweichen, brauchen wir einen sehr guten Grund.

(C) Brandeis Consulting.

Laufzeit und Risikostufe - Einstellungen

Bei der Definition der Klasse wird angegeben, welche Laufzeit erwartet wird und wie hoch die Risikostufe ist.

Laufzeiten (DURATION):

  • SHORT - wenige Sekunden
  • MEDIUM - weniger als eine Minute
  • LONG - länger als eine Minute

Risikostufe (RISKLEVEL)

  • HARMLESS - Der Systemstatus wird nicht verändert
  • DANGEROUS - Test verändern persistente Daten
  • CRITITCAL - Systemeinstellungen oder Customizing Daten werden verändert
CLASS ltc_calculate_delta DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.
(C) Brandeis Consulting.

Setup - Fixture

Das zu testende Objekt (CUT) wird vor dem Test erzeugt und mit einem fest definierten Zustand (aka. Fixture) gebracht.

Falls dieser Schritt nicht trivial ist, wird dazu die Methode SETUP() definiert. Diese wird vor der Ausführung jeder Testmethode vom UnitTest Framework aufgerufen.

Analog dazu wird, falls vorhanden, am Ende die TEARDOWN() Methode aufgerufen. Hier können Dinge nach dem Test aufgeräumt werden.

(C) Brandeis Consulting.

Testmethoden

In den Testklassen werden die privaten Testmethoden definiert. Diese haben keine Parameter. Es können aber Ausnahmen definiert werden, um Laufzeitfehler zu vermeiden.

CLASS ltc_calculate_delta DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    METHODS:
      t_single_quant FOR TESTING RAISING cx_static_check.
ENDCLASS.

Testklassen können auch normale Methoden enthalten, die nicht vom UnitTest Framework aufgerufen werden sollen. Damit können wir unsere Tests besser strukturieren und wiederholenden Code vermeiden.

(C) Brandeis Consulting.

Zeitlicher Ablauf bei Unit Tests

  • Schleife über alle Testklassen einer Klasse
    • Aufruf der statischen Methode CLASS_SETUP()
    • Schleife über alle Testmethoden
      • Erzeugung einer Instanz der Testklasse
      • Aufruf der Instanzmethode SETUP()
      • Aufruf der Testmethode
      • Aufruf der Instanzmethode TEARDOWN()
    • Aufruf der statischen Methode CLASS_TEARDOWN()
    • Ausführen von ROLLBACK WORK
(C) Brandeis Consulting.

Implementierung von Testmethoden

Die Klasse CL_ABAP_UNIT_ASSERT stellt Methoden bereit, mit denen wir unsere Testergebnisse prüfen und dem UnitTest Framework mitteilen können. Beispielsweise

  • ASSERT_EQUALS - prüft auf Gleichheit
  • ASSERT_TRUE bzw. ASSERT_FALSE - prüft logische Aussagen
  • FAIL - Test ist Fehlgeschlagen
    ...

(C) Brandeis Consulting.

Methoden der Klasse CL_ABAP_UNIT_ASSERT

Die meisten Methoden der Klasse haben den gleichen Parameter:

    METHODS  assert_Equals
        importing   value(act)              type any
                    value(exp)              type any
                    ignore_Hash_Sequence    type abap_Bool default abap_False
                    tol                     type f optional
                    msg                     type csequence optional
                    level                   type int1 default if_Abap_Unit_Constant=>severity-medium
                    quit                    type int1 default if_Abap_Unit_Constant=>quit-test
        returning   value(assertion_Failed) type abap_Bool,
(C) Brandeis Consulting.

Testabdeckung

Die UnitTests können auch mit Messung der Testabdeckung ausgeführt werden. Eine hohe Testabdeckung ist wünschenswert. Lücken in der Testabdeckung sind Hinweise auf fehlende Testszenarien.

(C) Brandeis Consulting.

Was macht gute Tests aus

  • Die Durchführung ist schnell
  • Es werden alle Aspekte abgefragt:
    • Happy Path: Die gewünschte Funktion
    • Fehlersituationen testen: Leere Parameter, negative Eingaben, etc.
  • Alle Zweige innerhalb des CUT sollen getestet worden sein
(C) Brandeis Consulting.

Wiederverwendung von Code in UnitTests

Auch in UnitTests gilt das DRY Prinzip: Nichts wiederholen. Entsprechend wird sich wiederholender Code in Methoden ausgelagert.

Hilfsmethoden

Die eigentliche Testmethode soll vor allem die Daten vorher und nachher zeigen. Und sich darauf fokussieren, was jeweils in den Methoden unterschiedlich ist.
Dazu können Hilfsmethoden verwendet werden, die die eigentliche Testlogik abbilden und die von den Testmethoden aufgerufen werden.

Globale Testklassen

Es können auch globale Klassen als FOR TESTING markiert werden. Diese können aber nicht direkt ausgeführt werden. Sie können aber von lokalen Testklassen beerbt werden.

(C) Brandeis Consulting.

Zugriff von Testklassen auf Private Daten

CLASS ltc_board DEFINITION DEFERRED  .

CLASS zbc_board DEFINITION LOCAL FRIENDS ltc_board.

CLASS ltc_board DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.
(C) Brandeis Consulting.

Test-Doubles

Aus dem Blog: ABAP Test Double Framework - An Introduction

(C) Brandeis Consulting.

Unterschiedliche Test-Doubles

Es gibt unterschiedliche Arten von Test-Doubles:

  • Dummy - Ein leeres Objekt, das keine Funktion hat. Außer das es die notwendigen Schnittstellen hat.
  • Stub - Ein Objekt das vorgegebene Daten zurückgibt
  • Mock-Objekt - Ein Objekt das komplexere Logik implementiert

Test Doubles können manuell erzeugt werden. Das ist aber aufwändig. Alternativ gibt es mehrere Mocking-Frameworks:

(C) Brandeis Consulting.

Das SAP Test Double Framework

Für alle Objekte, die per Interface referenziert werden, kann man ein Test-Double erstellen. Dazu ruft man im einfachsten Falle die Methode
CL_ABAP_TESTDOUBLE=>CREATE( <Interfacename> )
auf. Der RETURN Wert dieser Methode kann auf eine passende Referenzvariable gecastet werden:

    DATA lo_sorter_double TYPE REF TO zif_sort.

    "Erzeuge ein Test Double Objekt:
    lo_sorter_double = CAST zif_sort( cl_abap_testdouble=>create( 'ZIF_SORT' ) ).

Die Implementierung ist zunächst Dummy, d.h. ohne Funktion. Alle Methoden des Interfaces lassen sich aufrufen, aber es wird nie etwas zurückgegeben.

(C) Brandeis Consulting.

Vom Dummy zum Stub

Wenn das Double Objekt Daten zurückgeben soll, dann müssen die ganz konkreten Methodenaufrufe zuvor konfiguriert werden:

  1. Zunächst wird der Rückgabewert definiert
  2. Danach wird dann in einem Aufruf des Dummy Objektes mit den entsprechenden Parameterwerten festgelegt, wann die zuvor definierten Rückgabewerte zurückgegeben werden solle.
    "Definiere Rückgabewert....
    cl_abap_testdouble=>configure_call( lo_sorter_double )->returning( 
                         VALUE zif_sort=>tt_sort(  ( `Ali` )
                                                   ( `Jörg` )
                                                  ( `Manfred` ) ) ).
    "... für diesen Methodenaufruf:
    lo_sorter_double->sort( VALUE #( ( `Jörg` )
                                     ( `Ali` )
                                     ( `Manfred` ) ) ).

Für alle anderen Werte werden initiale Daten zurückgegeben.

(C) Brandeis Consulting.

Vollständiges Beispiel mit Test-Double

CLASS ltcl_test DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.
  PUBLIC SECTION.
    METHODS constructor.
    
  PRIVATE SECTION.
    DATA cut TYPE REF TO zcl_call_sort.
    METHODS:
      test_create_sorted_list FOR TESTING RAISING cx_static_check    .
ENDCLASS.


CLASS ltcl_test IMPLEMENTATION.

  METHOD test_create_sorted_list.
    DATA(result) = cut->create_sorted_list( VALUE #( ( `Jörg` )
                                                     ( `Ali` )
                                                     ( `Manfred` ) ) ).

    cl_abap_unit_assert=>assert_equals( act = result
                                        exp = `Ali, Jörg, Manfred` ).
  ENDMETHOD.

  METHOD constructor.
    DATA lo_sorter_double TYPE REF TO zif_sort.

    "Erzeuge ein Test Double Objekt:
    lo_sorter_double = CAST zif_sort( cl_abap_testdouble=>create( 'ZIF_SORT' ) ).

    "Definiere Rückgabewert....
    cl_abap_testdouble=>configure_call( lo_sorter_double )->returning( 
                         VALUE zif_sort=>tt_sort(  ( `Ali` )
                                                   ( `Jörg` )
                                                  ( `Manfred` ) ) ).
    "... für diesen Methodenaufruf:
    lo_sorter_double->sort( VALUE #( ( `Jörg` )
                                     ( `Ali` )
                                     ( `Manfred` ) ) ).

    cut = NEW zcl_call_sort( lo_sorter_double ).

  ENDMETHOD.

ENDCLASS.
(C) Brandeis Consulting.

ABAP SQL Test Double Framework


    methods select_data 
       importing iv_project type zbc_project_key
       RETURNING VALUE(result) type tt_tasks.
...
  METHOD SELECT_DATA.

    select *
      from zbc_tasks
      where project = @iv_project
      into table @result .


  ENDMETHOD.
  method t_select.

    data(environment) = cl_osql_test_environment=>create( 
      i_dependency_list = value #( (  'ZBC_TASKS' ) ) ).
    data(mock_data)   = 
    value zbc_ut_demo=>tt_tasks( client = sy-mandt 
     ( project = 'A' task_key = 'A-1' )
                            ( project = 'B' task_key = 'B-1' )
                            ( project = 'A' task_key = 'A-2' ) )  .
    environment->insert_test_data( i_data = mock_data ).
    data(result) = new zbc_ut_demo( )->select_data( 'A' ).

    cl_abap_unit_assert=>assert_equals( exp = value zbc_ut_demo=>tt_tasks(
       client = sy-mandt ( project = 'A' task_key = 'A-1' )
       ( project = 'A' task_key = 'A-2' ) )
                                        act = result ).

  endmethod.
(C) Brandeis Consulting.

Test Driven Development (TDD)

Unter Test Driven Development versteht man ein Vorgehen, bei dem zunächst ein Test geschrieben wird, bevor der produktive Code erstellt wird. Wenn alle Tests grün sind, muss zunächst ein neuer Test geschrieben werden der einen Fehler aufzeigt. Nur dann darf man weiter programmieren.

Mikrozyklus beim TDD

  1. Schreibe einen Test, der einen neuen Aspekt abdeckt und der aktuell noch auf rot läuft
  2. Korrigiere den Code so, das alle Test auf grün laufen
  3. Mache ein Refactoring Deines Codes, so dass
    1. Der Code so sauber, klar und einfach wie möglich ist und
    2. Weiterhin alle Tests grün sind

Beginne von vorne....

Die Dauer eines solchen Mikrozyklus sollte höchstens wenige Minuten betragen.

(C) Brandeis Consulting.

Testbarer Code

Viele Clean Code Prinzipien erleichtern die Erstellung von testbarem Code. Beispiele:

  • Kleine Methoden mit genau einer Aufgabe - Klare Testfälle
  • Wenige Parameter - Wenige Tests pro Methode notwendig, weil es nur wenige Kombinationen gibt
  • Trennung durch Interfaces - Das Test Double Framework kann eingesetzt werden
  • Klassen mit nur einer Verantwortlichkeit - Eine Trennung eines betriebswirtschaftlichen Objektes (z.B. Benutzer) in mehrere Klassen mit technischen Verantwortlichkeiten kann hilfreich sein, weil beispielsweise die Klasse für den Datenbankzugriff durch ein Test Double ersetzt werden kann

Grundsätzlich bauen wir keine "Abzweigungen" in den Code ein, damit die Tests leichter fallen:

IF p_is_test.
  SELECT * 
    FROM <Tabelle>
    INTO TABLE @lt_table.
ELSE. 
  lt_table = VALUE #( ( salesorderdocument = '10000' 
                        AMOUNT = '123,56'
                        ... ))
ENDIF. 
(C) Brandeis Consulting.