ABAP Unit

Muster und Codestruktur

(C) Brandeis Consulting.

Warum diese Lektion über Codestruktur

Man hat bei Methoden immer wieder die gleichen Herausforderungen, für die sich ein paar Entwurfsmuster bewährt haben.

Die Grundsätze:

  • Schnelle Testausführung
  • Produktiven Code nicht verbiegen, damit ein Test stattfinden kann
  • Entkoppeln von Abhängigkeiten
(C) Brandeis Consulting.

AAA

Die Grundstruktur jedes Tests.

  • Arange
  • Act
  • Assert
(C) Brandeis Consulting.

Testen auf Rückgabewerte

Im Einfachsten Falle haben wir bei es beim CUT mit einem reinen EVA Prinzip zu tun:

  • Eingabe
  • Verarbeitung
  • Ausgabe

Dann können wir direkt die Ausgabe testen. Typische Beispiele hierfür sind Funktionale Methoden mit RETURNING-Parameter

(C) Brandeis Consulting.

Dependency Injection

Die zu testende Klassen hängt von anderen Objekten ab. Damit wir den Test davon isolieren können, müssen wir diese ersetzen durch Doubles. Wenn die zu testende Klasse die Abhängigkeiten nicht selber erzeugt, sondern von Aussen übergeben bekommt, dann nennen wir das Dependency Injection

Arten von Dependency Injection

  • Public Injection
    • Constructor Dependency Injection - Abhängige Objekte werden im Constructor übergeben
    • Setter Injection - Mit einer Setter-Methode wird das DOC übergeben
    • Parameter Injection - Das DOC wird als Parameter der zu testenden Methode mitgegeben
  • Private Dependency Injection - Die Abhängigkeit wird in einem privaten Attribut gespeichert. Damit dieses von Aussen gesetzt werden kann, erklärt die CUT Freundschaft zur Testklasse.

Darüber hinaus gibt es noch die Möglichkeit, mit Test Seamsdie Abhängigkeiten zu ersetzen.

(C) Brandeis Consulting.

Testen auf Ausnahmen

Ganz wichtig bei Unit Tests ist das Verhalten, wenn nicht der Happy Path beschritten wird. Also immer, wenn eine Unit of Work die Arbeit nicht wie gewünscht erledigen kann.

Im Gegensatz zu anderen xUnit Frameworks gibt es bei AUNIT keine spezielle Exceptions Logik.

  METHOD create_duplicate_user.
    go_sql_environment->clear_doubles( ).
    mo_cut->create_new_user( VALUE #( user_id = 'ABC' ) ).

    TRY.
        mo_cut->create_new_user( VALUE #( user_id = 'ABC' ) ).
        
      " Die vorherige Anweisung soll eine Execption erzeugen. 
      " Damit darf das Programm hier nicht vorbei kommen. Falls doch: 
        cl_abap_unit_assert=>fail( msg = 'Creation of duplicate User should fail!' ). 

      CATCH zcx_duplicate.
        "Die Exception ist der erwartete/gewünschte  Ausgang dieses Tests! 
    ENDTRY.
  ENDMETHOD.
(C) Brandeis Consulting.

Testen auf Zustandsänderungen

Manchmal ändern Methode den internen Zustand eines Objektes, ohne ein Ergebnis zurückzugeben. Ob dieser Zuständsänderung erfolgreich war, kann man auf zweierlei Arten prüfen.

Der Test prüft den internen Zustand direkt

Beispielsweise wird geprüft, ob ein Attribut einen bestimmten Wert hat. Einfaches Beispiel:

mo_cut->set_msgv1( 'ABC').
cl_abap_unit_assert=>assert_equals( act = mo_cut->mv_msgv1
                                    exp = 'ABC' ). 

Nachteile

  • Der Test muss die Implementierung des CUT kennen
  • Änderungen der Interna führen zu einem (Syntax-)Fehler, auch wenn das Gesamtverhalten korrekt ist
  • Oft muss für die Zustandsprüfung der Code geändert werden. Z.B. durch Änderung der Sichtbarkeit eines Attributes oder durch Freundschaftserklärungen

Der Test prüft das Verhalten

Wegen der Zustandsänderung soll sich das Objekt ja anders verhalten als vorher. Es wird entsprechend das der Zustandsänderung erzeugte, neue Verhalten anhand einer öffentlichen Schnittstelle getestet.

mo_cut->set_msgv1( 'ABC').
cl_abap_unit_assert=>assert_cp( act = mo_cut->get_message()
                                exp = '*ABC*' ). 
(C) Brandeis Consulting.

Testen gegen den Datenbankzustand

Alle Anwendungen auf dem ABAP System hängen mehr oder weniger vom Datenbankzustand ab. Dieser ändert sich aber stetig. Damit ist er nicht dafür geeignet, dass man dagegen testet.

Um die Abhängigkeit vom Datenbankzustand zu entfernen, gibt es mehrere Möglichkeiten.

Isolation in einer Methode

Alle Datenbankzugriffe stehen in jeweils einer Methode. Für die Tests wird dann die Klasse abgeleitet und diese Methoden redefiniert.

Das war vor den Mock-Frameworks ein übliches Vorgehen.

Nachteile

  • Sichtbarkeit der Methoden ist falsch (protected statt private)
  • Redefinition ist notwendig
  • Funktioniert nur für Tests auf Klassenebene

Isolation in einer Klasse

Die sogenannten Data Access Objects (DAO) kapseln alle Datenbankzugriffe.

  • DAOs lassen sich zum Testen leicht ersetzen

SQL Mock Framework

Wir ersetzen für die einzelnen Tests den Datenbankzustand. Das genaue Vorgehen besprechen wir bei dem Thema Test Doubles.

Nachteil: Es handelt sich um echte Datenbankzugriffe, die ggf. langsamer sind.

(C) Brandeis Consulting.

Data Access Objects (DAO)

Wie auf der letzte Folie gezeigt, sind DAOs eine gute Option zum Ersetzen des Datenbankzustands während des Tests. Ein DAO soll eigentlich nur die Datenbankzugriffe enthalten und möglichst schlank sein.

Mit dem modernen ABAP SQL kann man, gerade beim lesenden Zugriff, viel Logik in die Datenbank drücken (Code-To-Data). Davon sollte man unbedingt Gebrauch machen.

Test von DAOs

  • Der Test eines DAOs kann mit Hilfe des SAP SQL Test Double Frameworks erfolgen.
  • Die DAOs sollten unabhängig vom Rest der Komponente getestet werden, da dies Tests ggf. etwas länger laufen können.
(C) Brandeis Consulting.

Proxy-Objekte

Das Entkoppeln von anderen Frameworks oder von Legacy Code ist schon wegen des Clean Code Gedankens eine sinnvolle Idee. Mit Proxy-Objekten wird die Anzahl der Abhängigkeiten minimiert. Ausserdem wird es einfach, diese durch Test-Doubles zu ersetzen.

(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 (aka. Testinfrastruktur)

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.

Auslagern von Hilfsmethoden in Superklassen

Die Hilfsmethoden können in eine Superklasse ausgelagert werden. Damit

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

Test Driven Maintenance

Eine Variante von TDD.

Für jeden Bug/Fehler, egal ob per Ticket gemeldet oder selber entdeckt, wird zunächst ein Test angelegt, der Rot ist. Durch das beheben des Fehlers soll der Test grün werden.

Vorteil

  • Ich kann jederzeit damit anfangen
  • Die Testabdeckung der Codebasis wird auch für bestehende Anwendungen langsam besser
(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
  • 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.