Nach längerer Vorbereitungszeit bin ich nun endlich soweit, den nächsten Schritt in dem System zur Formulierung von Akzeptanztests vorzustellen. Dabei geht es um die Generierung von (nutzbarem) Quellcode, im speziellen Fall Java-Code, aus den in einer DSL erstellten Dateien für die Formulierung des Anfangs- und Endzustands für Akzeptanztests.
Grundlegender Ablauf der Code-Generierung
Ziel der Code-Generierung ist es, aus dem domänen-spezifischen DSL-Text ausführbaren Quellcode zu erstellen. Dabei werden die Informationen aus der DSL Datei (bzw. aus dem dahinter liegenden Modell) übernommen und mit anderen wichtigen Daten angereichert, sodass letztendlich eine Datei mit Quellcode entsteht, die völlig selbstständig funktionieren kann.
Nachfolgende Abbildung soll den Aufbau bzw. Ablauf der Code-Generierung verdeutlichen.
Als Anfang dienen die beiden DSL-Dateien für den Anfangs- und Endzustand des Testfalles, welche im Schritt 3 beschrieben wurde. Diese basieren auf einem Meta-Modell, welches für die Akzeptanztestsprache geschrieben wurde.
Anhand dieses Meta-Modells kann ein Code-Generierungs-Template den Inhalt der letztendlich zu erzeugenden Dateien aufbauen. Bei der konkreten Generierung werden die DSL-Dateien dem Generator übergeben, woraufhin dieser mit Hilfe des Templates jeweils für den Anfangs- und den Endzustand Code-Dateien erzeugt.
Was ist nun der Generator genau? Im Prinzip muss es sich hier um eine Art Black-Box handeln, also eine Komponente, die ohne Zutun oder Konfiguration des Benutzers, die Code-Generierung durchführt. Der Generator kümmert sich darum, die DSL-Dateien einzulesen und das Template auf diese anzuwenden.
Übrigens: nicht zu vergessen ist neben dem Anfangs- und Endzustand des Testfalles noch eine dritte DSL-Datei: der Ausgangszustand. Da wird momentan noch nicht ganz wissen, wo dieser am besten hingehört, zum Testfall oder zum Use Case, lasse ich ihn der Einfachheit halber hier erst einmal weg. Jedoch ist die Hinzunahme kein wirkliches Problem. Man hätte dann nur eben drei DSL-Dateien, für die Code generiert wird.
Code-Generierung mit dem Use Case “Buchung erfolgreich”
Auch hier greife ich wieder den Use-Case aus dem vorherigen Schritt auf. Dabei handelt es sich um den Use Case “eine erfolgreichen Zimmerbuchung wird durchgeführt” aus der Domäne “Buchungssystem”.
Hier ein kurzer Blick auf die entsprechende Projektstruktur:
Auf den Inhalt der beiden DSL-Dateien möchte ich aus Platzmangel verzichten, man findet sich in einem der letzten Posts. Kurz zusammengefasst: im Anfangszustand wird ein Hotel mit einem bestimmten Zimmer erzeugt, sowie ein Gast. Im Endzustand wird dann überprüft, ob es eine Buchung auf das entsprechende Zimmer (es ist das einzige) gibt. Desweiteren wurde hier eine Definitions.atdsl Datei erzeugt, in welche eine Definition ausgelagert wurde. Dies ist aber nicht von Wichtigkeit für das Beispiel.
Des Weiteren sieht man im Bild das Domänen-Modell (Hotel.ecore) und die MWE-Datei zum Starten des Generators (RunAcceptanceTest1.mwe bzw. properties)
Code für Domänen-Modell muss (noch) manuell erzeugt werden
Damit die am Ende erzeugten Java-Dateien lauffähig sind, muss Code für das Domänen-Modell erzeugt werden. Damit sind die im obigen Bild gezeigten Packages “de.saxsys.dsl.hotel” und seine Subpackages gemeint. Diese Erzeugung soll in Zukunft automatisch geschehen. Jedoch muss sich der Benutzer im Moment selbst damit beschäftigen. Wie diese Standard-EMF-Prozedur abläuft, kann u.a. hier nachgelesen werden: EMF Developer Guide - Generating an EMF Model. Anstatt ein Rose-Model oder Annotated Java verwendet man sein Ecore-Model als Basis für das Generator-Model.
Wichtig ist, dass man im Generator-Model ein Package für die Klassen angibt, wie im folgenden Screenshot zu sehen:
Es hat sich in der Praxis als beste Lösung erwiesen, wenn das Prefix groß geschrieben wird und der Package-Name im Ecore-Model klein geschrieben. Dadurch erhält man Java-Klassen, deren Namen sich an die gültigen Konventionen halten.
Der Generator (Xpand2, MWE)
Um die Generierung des Codes für den Anfangs- und Endzustand zu starten, liegt bereits ein vom Assistenten erzeugtes MWE-File vor. Der Inhalt dieses Workflows ist für den Benutzer nicht wichtig. Es wird lediglich ein anderer Workflow in einem Eclipse-Plugin des Prototyps aufgerufen. Dieser wiederum ruft den Xpand2-Generator auf, welcher mit Hilfe des Templates den Java-Code aus den DSL-Dateien erzeugt.
Der Benutzer muss aber die zugehörige Properties-Datei kennen und bearbeiten. Hier der Inhalt der Datei “RunAcceptanceTest1.properties”:
beforeURI=classpath:/de/saxsys/dsl/at/usecase1/acceptancetest1/AcceptanceTest1_before.atdsl
afterURI=classpath:/de/saxsys/dsl/at/usecase1/acceptancetest1/AcceptanceTest1_after.atdsl
modelName=Hotel
modelPackage=de.saxsys.dsl.hotel
testName=AcceptanceTest1
Die URIs für die beiden DSL-Dateien sowie der Name des Testfalls sollten bereits eingetragen sein. Dadurch, dass der Benutzer manuell den Code des Domänen-Modells erzeugt, ist es jedoch nötig, die entsprechenden Angaben hier nachzutragen. Dazu gehört der Name des Modells (erster Buchstabe groß geschrieben) und das Package, in dem der Modell-Code liegt.
Durch das Starten der MWE-Datei wird die Code-Generierung gestartet.
Beispiel aus dem Xpand-Template
Da Xpand relativ umfangreich und das Template entsprechend groß ist, möchte ich hier nur exemplarisch darstellen, wie aus der DSL ein Stückchen Java-Code erzeugt wird.
Folgender Abschnitt zeigt, wie für ein Objekt aus der DSL (RegularModelObject – ein Objekt basierend auf dem Domänen-Modell, nicht auf einer Definition) Hibernate-Java-Code zum erzeugen dieses Objektes in der Datenbank beschrieben wird:
«FOREACH modelObjects.typeSelect(RegularModelObject) AS e ITERATOR i-»
«LET e.getVariableNameForRegular(i.counter1) AS varName-»
«e.type.name» «varName» = «getGlobalFactoryName()».eINSTANCE.create«e.type.name»();
«FOREACH e.attributes.typeSelect(AttributeString) AS aString-»
«varName».set«aString.attribute.name»("«aString.value»");
«ENDFOREACH-»
...
session.save(«varName»);
«ENDLET»
«ENDFOREACH»
Dabei wird zunächst durch eine LET-Anweisung der Name des Objektes bestimmt. Wurde in der DSL ein Name angegeben, wird dieser verwendet, ansonsten wird der Klassenname dafür mit einem Counter hochgezählt. Dies macht die Extension getVariableNameForRegular. Danach wird für jedes Attribut dieses Objektes (hier nur Attribute mit String-Werten) ein entsprechender Setter gesetzt. Zum Schluss wird noch ein session.save ausgegeben.
Der dazu erzeugte Java-Code sieht folgendermaßen aus:
Zimmer zimmer1 = HotelFactory.eINSTANCE.createZimmer();Dies sind zwei Objekte aus dem Anfangszustand unseren Beispiel-Use-Cases. So oder so ähnlich werden alle Objekte in Java-Code überführt, je nachdem, ob es sich um ein Insert handelt, oder ein Select. Dazu kommt noch entsprechender Code zum Initialisieren der Datenbank-Verbindung.
zimmer1.setZimmernummer("1");
zimmer1.setZimmerart(Zimmerart.EINZELZIMMER);
session.save(zimmer1);
Gast gast1 = HotelFactory.eINSTANCE.createGast();
gast1.setVorname("Max");
gast1.setNachname("Mustermann");
gast1.setEmail("Max@Mustermann.de");
session.save(gast1);
Resultierender Code
Die erzeugten Java-Codedateien befinden sich im src-gen Ordner und können manuell über ihre main-Methode aufgerufen werden.
Ein Beispiel für Inserts haben wir bereits gesehen. Das Select aus dem Endzustand des Beispiel Use-Cases sieht in Java folgendermaßen aus
Query query = session.createQuery("SELECT buchung "
+ "FROM Buchung buchung " + ",Zimmer zimmer " + ",Gast gast "
+ "WHERE " + "buchung.Anreisedatum=:anreisedatum "
+ "AND " + "buchung.Abreisedatum=:abreisedatum " + "AND "
+ "buchung.Leistungsart='Vollpension' " + "AND "
+ "buchung.Zimmer=zimmer " + "AND "
+ "zimmer.Zimmernummer='1' " + "AND "
+ "zimmer.Zimmerart='Einzelzimmer' " + "AND "
+ "buchung.Gaeste=gast " + "AND "
+ "gast.Vorname='Max' " + "AND "
+ "gast.Nachname='Mustermann' " + "AND "
+ "gast.Email='Max@Mustermann.de' ");
query.setParameter("anreisedatum", java.sql.Date.valueOf("2010-07-01"));
query.setParameter("abreisedatum", java.sql.Date.valueOf("2010-07-05"));
List<?> buchungList = query.list();
// AssertNotNull: check if there is a result
assert !buchungList.isEmpty();
Hier wird ein Hibernate-Query erzeugt, in dem nach der Buchung gesucht wird. Ist die Ergebnismenge leer, so schlägt der Test fehl. Dazu wird das Java-eigene assert-Kommando genutzt.
Der etwas kryptische Zusammenbau des Queries ergibt sich aus dem Xpand-Template. Hierbei ist zu beachten, dass ich der Einfachheit halber die Referenzen von Buchung zu Gast und Zimmer auf genau eine Referenz begrenzt habe. Das heißt, eine Buchung hat nur einen Gast und ein Zimmer. Bei mehreren Referenzen würde sich das Query höchstwahrscheinlich ändern.
Zu beachten ist auch, dass Attribute mit einem Date-Wert als Parameter eingebunden werden.
Abschließend…
Dadurch, dass die Java-Dateien selbstständig aufgerufen werden können, könnte man nun bereits Akzeptanztests damit durchführen. Jedoch müsste man jeweils Anfangs- und Endzustand und die zu prüfende Funktion oder das Programm manuell ausführen. Um dies zu automatisieren, wird es einen fünften Schritt geben, der sich mit einer Ausführungsumgebung beschäftigt, mit der es möglich sein soll, Akzeptanztests einmal manuell aus der Entwicklungsumgebung heraus, aber auch automatisch aus einem Skript (z.B. für Regressionstests) aufzurufen.