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.
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.
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.
... Critical Assertion Error: 'Discount Calculation: Discount on Slayer Albums for VIP Slayer fans (exclusive contract with BMG)' ...
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.
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:
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.
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/' ). ...
... configure( pattern = '^the discount is (.+)% \\m\/$' method_name = 'evalua-te_applied_discount' ). ...
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.
Sie fragen sich, wie Sie Cacamber in Ihren Entwicklungsprozess einbinden? Ich empfehle Ihnen wie folgt vorzugehen:
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 ABAP. Den ursprünglichen Text, verfasst in Englisch, finden Sie hier: BDD-style tests for ABAP.