Cacamber als BDD-Framework für ABAP: Ein OSS-Hobbyprojekt unseres Entwicklungsleiters

Team @ INTENSE
SAP for Utilities

Kennen Sie folgendes Problem? Neue Funktionen sollen entwickelt werden, aber die Kommunikation mit dem Kunden ist schwierig. Leute reden aneinander vorbei, Anforderungen sind unklar. Und wenn die Funktion schließlich implementiert ist, stellt sich heraus, dass der Kunde eigentlich eine andere Anforderung hatte: "Aber wir dachten, das macht etwas ganz anderes!"

Die Lösung für dieses Problem: Die Kommunikation zwischen dem Kunden und dem Entwicklungsteam zu intensivieren und so früh wie möglich Feedback einzuholen. Ein agiles Prozessmodell, das sich auf dieses Vorgehen konzentriert, ist verhaltensgetriebene Softwareentwicklung, eher bekannt als Behavior Driven Development (kurz: BDD). BDD stellt das Verhalten des Nutzers in den Mittelpunkt und versucht, Konsistenz zwischen Anforderungen, Implementierung und Tests herzustellen - ein gemeinsames Verständnis. Dazu werden Beispiele, sogenannte Szenarien, spezifiziert, die das Verhalten beschreiben. Dies geschieht häufig im Rahmen von "Three Amigos Sessions", in denen Product Owner (oder Business Analyst), Entwickler und Tester zusammenarbeiten, um Unklarheiten möglichst frühzeitig zu beseitigen.

BDD ist der akzeptanztestgetriebenen Softwareentwicklung (Acceptance Test-driven Software Development, kurz: ATDD) sehr ähnlich. Darüber hinaus liegt der Schwerpunkt auf der Automatisierung: Szenarien sollen als ausführbare Spezifikationen dienen – sie sollen automatisch testbar sein.

Szenarien beim Behavior Driven Development

Szenarien werden in einer speziellen Sprache namens Gherkin geschrieben, wie beispielsweise dieses Szenario für ein Online-Shop-System:

Feature: Discount calculation Scenario: Discount on Slayer albums for VIP Slayer fans (exclusive contract with BMG) GIVEN the customers first name is Dominik and his last name is Panzer AND his birthdate according to our CRM system is 06.06.2006 WHEN the sales clerk lets the system calculate the customers discount on a Slayer album THEN the discount is 66% \m/

Sie kennen vielleicht schon Gherkin und das GIVEN ... WHEN ... THEN-Muster. Es ist dem ARRANGE ... ACT ... ASSERT sehr ähnlich. Beides sind Techniken, um Unit-Tests eine klare Struktur zu geben.

In ABAP sieht das normalerweise so aus: Der Entwickler wählt eine Funktion aus, erstellt eine Testklasse, gibt der Testmethode einen entsprechenden Namen und schreibt einen Akzeptanztest:

METHOD discount_calculation.
* Given
sut->do_this( parameter ).
sut->do_that( parameter ).
"more complex stuff here
DATA(product) = |Slayer Album|.

* When DATA(discount) = sut->calculate( product ). * Then cl_abap_unit_assert=>assert_equals( exp = 66 act = discount ). ENDMETHOD

GIVEN wird für die Erstellung des Tests verwendet. WHEN legt fest, welche Aktion durchgeführt wird, und THEN ist für die Durchsetzung verantwortlich.

Aber die GIVEN... THEN... WHEN... Kommentare sind hier nicht sehr hilfreich. Außerdem sind die einzelnen Teile des Tests nicht wiederverwendbar. Und der gesamte Test ist nicht sehr domänenzentriert im Vergleich zu dem Szenario, das das Unternehmen tatsächlich definiert hat.

Also wollten es einige Leute besser machen und begannen, ihre Tests so zu schreiben:

METHOD discount_on_slayer_albums.
given_cstmr_first_last_brthdy( EXPORTING first_name = 'Dominik'
                                         last_name = 'Panzer'
                                         birthdate = '20060606' ).
when_the_price_is_calculated_for( 'Slayer Album' ).
then_the_discount_is_correct( ).
ENDMETHOD

Das ist viel besser! Die meisten Details und die Komplexität sind hinter gut benannten Methoden versteckt – es wurde eine weitere Abstraktionsebene eingeführt. Außerdem gibt es die Möglichkeit, die Testparameter zu ändern. Das macht die Testschritte flexibler und wiederverwendbar.

Die Verwendung parameterloser Methoden würde den Code jedoch lesbarer machen. Doch diese Lösung kommt unserem ursprünglichen BDD-Szenario in keiner Weise nahe. Es handelt sich hier nicht um natürliche Sprache, sondern hauptsächlich um Code. Außerdem wird dieser Ansatz durch die maximale Methodenlänge von ABAP eingeschränkt. Die Entwickler sind gezwungen, Abkürzungen usw. zu verwenden.

Open Source Software in Eigenregie entwickelt: Eine BDD-Schicht namens "Cacamber“

Aus diesem Grund beschloss ich, dies zu ändern und ein Open Source Software Hobbyprojekt zu starten. Ich entwickelte eine BDD-Schicht für ABAP namens "Cacamber". Derzeit unterstützt Cacamber englische und deutsche Gherkin-Schlüsselwörter.

Wenn Sie sich entscheiden, Cacamber als Brücke zwischen dem in Gherkin geschriebenen Szenario und ABAP Unit zu verwenden, sehen Ihre Testschritte wie folgt aus:

METHOD discount_on_slayer_albums.
scenario( 'Discount on Slayer albums for VIP Slayer fans (exclusive contract with BMG)' ).

given( 'the customers first name is Dominik and his last name is Panzer' ).
and( 'his birthdate according to our CRM system is 06.06.2006' ).
when( 'the sales clerk lets the system calculate the customers discount on a Slayer album' ).
then( 'the discount is 66% \m/' ).
ENDMETHOD.

Wenn der Test fehlschlägt, informiert Sie ABAP Unit auf diese Weise:
...
Critical Assertion Error: 'Discount Calculation: Discount on Slayer Albums for VIP Slayer fans (exclusive contract with BMG)'
...
Ihre Tests können auch so aussehen:
METHOD discount_on_a_shopping_cart.
scenario( 'Discount voucher applied to whole shopping card' ).

given( 'the customers has the following items in his shopping cart:' &&
     '| 2 | Slayer | Reign In Blood | LP | 9,99€ |' &&
    '| 1 | Slayer | South Of Heaven | LP | 9,99€ |' &&
    '| 1 | David Hasselhoff | Crazy For You | LP | 9,99€ |' ).
and( 'he adds a valid 10% voucher' ).
when( 'the customer checks out' ).
then( 'the discount is 4€.' ).
ENDMETHOD.

Oder so:

METHOD no_discount_on_shopping_cart.
verify( 'Scenario: Customer is not eligible for a discount on the shopping cart' &&
        'Given the customers first name is Dominik and his last name is Panzer' &&
        'And his birthdate according to our CRM system is 06.06.2006' &&
        'And in his shopping cart are the following items:' &&
        '| 1 | Scooter - Hyper Hyper |' &&
        '| 1 | Scooter - How Much Is The Fish |' &&
        '| 1 | Scooter - Maria (I like it loud) |' &&
        'When the sales clerk lets the system calculate the customers discount on the shopping cart' &&
        'Then the discount is 0% \m/' ).
ENDMETHOD.

Vorteile von Behavior Driven Development und Testautomatisierung mit Cacamber

Wie Sie sehen können, sind die vom Unternehmen definierten Szenarien (fast) 1:1 in Ihrem Code enthalten. BDD und eine derartige Testautomatisierung haben einige wesentliche Vorteile:

  • Verstärkt/verbessert die Zusammenarbeit zwischen Unternehmen, Entwicklung und QA
  • Erleichtert die Kommunikation im Team durch die Verwendung einer natürlichen, domänenzentrierten Sprache
  • Das Team kann über das Systemverhalten und nicht über technische Implementierungsdetails sprechen
  • Die Entwickler wissen, wann sie "fertig" sind, da die klar definierten Szenarien als Abnahmekriterien verwendet werden können
  • Die Testszenarien sind für neue Entwickler leicht lesbar, da sie in einer natürlichen Sprache geschrieben sind
  • Testschritte können wiederverwendet werden
  • Tests können leicht parametrisiert werden
  • Tests sind eine lebendige und aktuelle Dokumentation
  • Das Testen wird auf die linke Seite des Entwicklungsprozesses verlagert
  • Automatisierte BDD-Tests geben den Entwicklern die Sicherheit, dass der Code noch funktioniert und nichts kaputt geht
  • Fördert Unit-Tests zur Implementierung der Szenarien
  • Versteckt die Komplexität der Implementierungen hinter einer Abstraktionsschicht

Wie verfasst man eigene Tests im BDD-Stil in ABAP?

Wie das funktioniert und wie Sie Ihre eigenen Tests im BDD-Stil in ABAP schreiben können? Im Cacamber Repository gibt es eine umfangreiche Dokumentation und zwei Beispielklassen.

Im Großen und Ganzen gelingt es wie folgt:
Wenn Sie eine von Cacambers Methoden wie GIVEN, WHEN, THEN, etc. oder VERIFY aufrufen, geben Sie einen String als Parameter dieser Methoden an. Dieser String ist ein einzelner Schritt in Ihrem Test (wenn Sie GIVEN etc. verwenden) oder auch ein komplettes Szenario (wenn Sie VERIFY verwenden), geschrieben in natürlicher Sprache. Cacamber nimmt diesen String und vergleicht ihn mit der Konfiguration, die über die Methode CONFIGURE im SETUP Ihrer Testklasse bereitgestellt wurde. Die Konfiguration besteht aus verschiedenen Einträgen. Der erste Eintrag wird verwendet, um zu prüfen, ob der reguläre Ausdruck ("pattern") und die angegebene Zeichenkette übereinstimmen. Ist dies der Fall, extrahiert Cacamber die Variablen aus der Zeichenkette. Dann wird die Methode aufgerufen, die über die Konfiguration bereitgestellt wurde und die Variablen als Parameter verwendet. Stimmt kein regulärer Ausdruck überein, wird der nächste Konfigurationseintrag geprüft, usw. Wenn kein Konfigurationseintrag gefunden werden kann, wirft Cacamber eine Ausnahme.

Beispiel für einen BDD-style Test

Hier ein kurzes Beispiel. Schauen wir uns nun an, wie Cacamber den gegebenen Teil unseres Szenarios zerlegt:

METHOD discount_on_slayer_albums.
...
given( 'the customers first name is Dominik and his last name is Panzer' ).
...
ENDMETHOD.

Aus der Sicht der Unit-Tests wollen wir einen Vor- und einen Nachnamen erhalten und diese zum Einrichten unseres Tests verwenden. Unsere lokale Testklasse muss von ZCL_CACAMBER erben, damit Sie Cacambers Funktionen nutzen können.

Dann müssen wir Cacamber in der SETUP Methode unserer lokalen Testklasse mitteilen, welche Methode aufgerufen werden soll, wenn ein bestimmter Regex (Abkürzung für regulärer Ausdruck bzw. regular expression) übereinstimmt:

...
configure->( pattern = '^the customers first name is (.+) and his last name is (.+)$' methodname = 'set_first_and_second_name' ).
...

Diese Konfiguration ruft die Methode SET_FIRST_AND_SECOND_NAME auf, wenn der Regex PATTERN passt. Zusätzlich extrahiert sie die beiden Variablen aus dem Platzhalter "(.+)" und verwendet sie als Importparameter für SET_FIRST_AND_SECOND_NAME.

Die Methode sieht wie folgt aus:

...
PUBLIC SECTION.
METHODS set_first_and_second_name IMPORTING first_name TYPE char30
last_name TYPE char30.
...
METHOD set_first_and_second_name.
discount_calculator->set_first_name( first_name ).
discount_calculator->set_last_name( last_name ).
ENDMETHOD.
...

Innerhalb dieser Methoden können Sie die eigentliche Logik Ihrer Tests platzieren, zu Beispiel den Aufruf verschiedener Methoden Ihres Geschäftsobjekts.

Sie müssen auch Behauptungen für Ihren Unit Test aufstellen. Normalerweise geschieht dies mit dem Schlüsselwort THEN:

...
then( 'the discount is 66% \m/' ).
...
Wir weisen also Cacamber in unserer SETUP-Methode an, die Methode EVALUA-TE_APPLIED_DISCOUNT aufzurufen, wenn der folgende Regex passt:
...
configure( pattern = '^the discount is (.+)% \\m\/$' method_name = 'evalua-te_applied_discount' ).
...
Die Methode EVALUATE_APPLIED_DISCOUNT sieht wie folgt aus:
PUBLIC SECTION.
METHODS: evaluate_applied_discount IMPORTING expected TYPE int4.

METHOD evaluate_applied_discount.
cl_abap_unit_assert=>assert_equals( msg = |{ current_feature }: { cur-rent_scenario }| exp = expected act = discount ).
ENDMETHOD.

So einfach geht's: Definieren Sie ein Muster in der Konfiguration und teilen Sie Cacamber mit, welche öffentliche Methode Ihrer Testklasse aufgerufen werden soll.

Für weitere Details sehen Sie sich bitte die mitgelieferten Beispielklassen an.

So integrieren Sie Cacamber in Ihren Entwicklungsprozess

Sie fragen sich, wie Sie Cacamber in Ihren Entwicklungsprozess einbinden? Ich empfehle Ihnen wie folgt vorzugehen:

  1. Identifizieren Sie die wichtigen Funktionen Ihrer Software und setzen Sie Prioritäten.
  2. Identifizieren Sie verschiedene Szenarien für diese Funktionen und priorisieren Sie sie.
  3. Definieren Sie Akzeptanztestbeschreibungen für diese Szenarien. Verwenden Sie dazu die Gherkin-Sprache und stellen Sie sicher, dass das gesamte Team sie versteht. Szenarien sind keine technischen Beschreibungen. Sie beschreiben die Absicht des Benutzers, was er tatsächlich tut und was er erreichen will.
  4. Schreiben Sie das erste Szenario mit Cacamber, das aus den relevanten Schritten besteht, und setzen Sie ein ASSERT in die THEN-Stufenmethode.
  5. Die neue Testmethode wird höchstwahrscheinlich mit einer Ausnahme fehlschlagen, weil die Stufenmethoden nicht implementiert sind oder das ASSERT fehlgeschlagen ist. Sie könnte auch grünes Licht geben, wenn die Stufenmethoden bereits implementiert sind und die vorhandene Geschäftslogik die Kriterien des ASSERTs erfüllt. Dann sind Sie fertig.
  6. Wenn das Szenario fehlschlägt, müssen Sie höchstwahrscheinlich eine neue Methode für Ihre Geschäftslogik schreiben. Verwenden Sie dafür Test Driven Development (TDD): Red-Green-Refactor. Wenn Ihre TDD-Tests grün sind, fügen Sie die neu erstellte oder geänderte Methode in Ihre Stufenmethode ein. In einer einzelnen Stufenmethode kann es mehr als einen Methodenaufruf für Ihre Geschäftslogik geben. Sie müssen auch die Ergebnisse Ihrer Geschäftslogik in Attributen speichern, damit andere Stufenmethoden darauf zugreifen können (ein Schritt berechnet einen Wert, der nächste Schritt validiert ihn usw.)
  7. Wiederholen Sie den Vorgang, bis Ihr Szenario grün ist.
  8. Überarbeiten.
  9. Beginnen Sie mit dem nächsten Szenario und verwenden Sie Ihre Stufenmethoden wieder.

Ich hoffe, diese kurze Einführung hat Ihr Interesse geweckt. Ich freue mich auf Ihr Feedback, hier unter diesem Blogbeitrag oder Sie kontaktieren mich in der SAP Community: Dominik Panzer sowie auf Twitter / X: Dominik Panzer.


Der Beitrag entstand im Rahmen einer Veröffentlichung innerhalb der SAP Community und auf GitHub: Cacamber - The BDD-Framework for ABAPDen ursprünglichen Text, verfasst in Englisch, finden Sie hier: BDD-style tests for ABAP.