Dienstag, 20. Juli 2010

Generieren von Code aus der DSL (Schritt 4)

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.

Codegen01

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:

CodegenProject

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:

CodegenGenModel

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();
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);
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.

Resultierender Code

Die erzeugten Java-Codedateien befinden sich im src-gen Ordner und können manuell über ihre main-Methode aufgerufen werden.

CodegenResult

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.

Freitag, 9. Juli 2010

Umgang mit DB-Abfragen in der DSL

Einführung

Bevor die Schrittfolge zur Erstellung von Akzeptanztests weitergeführt wird, möchte ich an dieser Stelle gerne noch eine Problemstellung einschieben, welche mir vor kurzem aufgefallen ist. Es handelt sich grob gesagt um das Problem, dass der Benutzer der DSL nicht weiß, ob eine Abfrage der Datenbank (meist im Endzustand, z.B. “Finde Gast…”) genau ein Objekt zurückgibt, oder mehrere. Dies kann man gut mit einem SELECT-Befehl in SQL vergleichen. Prinzipiell gibt dieser ein Resultset mit beliebig vielen Zeilen zurück. Ebenso kann man dies auf die DSL übertragen.

Das bedeutet nun, dass:

  1. die DSL geeignete Sprachmittel zur Verfügung stellen muss, um eventuell zu überprüfen, wie groß die Ergebnismenge ist
  2. der Benutzer sich konkret damit auseinandersetzen muss, wie viele Objekte er als Ergebnis seiner Abfrage erwartet

Letzteres könnte man als Bestandteil der Fachmodellierung sehen, den der Benutzer sowieso betrachten muss. Insofern dürfte er keine Schwierigkeiten haben, die Anzahl der Elemente in der Ergebnismenge zu bestimmen.

Am besten verfolgt man das Ganze an Hand eines Beispiels:

Finde Buchung buchung1 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Vollpension
}
hat Zimmer: zimmer1
hat Gaeste: gast1

Nicht Null? buchung1

Die ist das Beispiel, welches ich bereits verwendet hatte, um die Formulierung der Akzeptanztests zu erläutern. In dem Beispiel bin ich davon ausgegangen, dass es nur eine Buchung geben kann, was in diesem Fall auch selbstverständlich sein sollte.

Jedoch könnte es passieren, dass man beispielsweise durch ein fehlerhaftes Programm mehrere Buchungen zurück bekommt, oder dies absichtlich durch ein Einschränken der Abfrage (z.B. indem man die Referenz auf Zimmer und Gast entfernt) hervorruft. Wie soll sich die DSL nun verhalten?

Verschiedene Assertions für Objekte und Listen

Mein erster Vorschlag ist, die Ergebnismenge in der DSL erst einmal gleich zu behandelt. Das heißt, die Variable buchung1 von oben kann ein einzelnes Objekt sein oder eine Liste von Buchungs-Objekten.

Nun kommt es auf die Assertion an, die man durchführen möchte. Es sollte sowohl Assertions für einzelne Objekte oder für Listen geben. Dabei gibt es für einzelne Objekte:

  • Nicht Null
  • Null
  • Gleich
  • Ungleich

Diese können nur verwendet werden, wenn es tatsächlich nur ein Objekt gibt, ansonsten schlägt der Testcase fehl. Das bedeutet auch, dass der Benutzer, wenn er einen Test schreibt, diese Assertions prinzipiell nur verwendet, wenn er der Meinung ist, dass hier nur ein Objekt zurückgegeben wird. Dies ist somit im gewissen Sinne auch Teil der Fachmodellierung.

Für Listen hingegen könnten folgende Assertions verwendet werden:

  • Beinhaltet
  • Beinhaltet nicht

Diese beiden sind je das Äquivalent zu Gleich und Ungleich. Sie überprüfen, ob ein bestimmtes Objekt in der Ergebnismenge vorhanden ist oder nicht. Hier ist noch zu sagen, dass eine Liste auch nur ein Objekt beinhalten kann, wobei sich das dann mit obigen Fall überschneidet. Das heißt, man kann prinzipiell immer annehmen, dass eine Liste von Objekten zurückgegeben wird und auf die entsprechenden Assertions zurückgreifen. Die Assertions für nur ein Objekt sind dann nur dazu da, zu erzwingen, dass es überhaupt nur ein Objekt geben darf.

Übrigens würde das System hinter der DSL immer mit Listen arbeiten. Für die Assertions Nicht Null, Null usw. würde dann immer zuerst geprüft werden, ob die Liste nur ein Element beinhaltet (oder leer ist).

Assertions für Größe der Ergebnismenge

Zusätzlich könnte die DSL direkte Assertions anbieten, die den Zustand der Ergebnismenge überprüfen, wie:

  • GenauEinElement?
  • MehrereElemente?
  • Leer?

Ich denke, die Begriffe sind selbsterklärend. Eventuell könnten die Namen ein bisschen eleganter ausfallen. Das Beispiel von oben könnte man nun folgendermaßen erweitern:

Finde Buchung buchung1 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Vollpension
}
hat Zimmer: zimmer1
hat Gaeste: gast1

GenauEinElement? buchung1

// ist jetzt eigentlich überflüssig
Nicht Null? buchung1

Dadurch, dass man nun prüft, ob die Ergebnismenge genau ein Element bzw. Objekt beinhaltet, ist die Nicht Null Anfrage eigentlich überflüssig. Hier müsste man nun evaluieren, wenn man diese Assertions einsetzt, ob man dann noch die speziell auf ein Element angepassten Assertions wie Null, Gleich usw. benötigt. Im Prinzip könnte man dann mit den Listen-Assertions arbeiten.

Dieses Beispiel soll nun noch einmal die Vergleichsvariante zeigen, die ich bis jetzt ein bisschen vernachlässigt habe:

Finde Buchung buchung1 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
}
hat Zimmer: zimmer1

// ist nur ein Objekt zurückgekommen (Zimmer nur einmal gebucht)
GenauEinElement? buchung1

// das Ergebnis muss genau diese Buchung beinhalten
Beinhaltet? buchung1 vergleichsBuchung
// oder alternativ:
Gleich? buchung1 vergleichsBuchung

// diese Buchung muss es sein
Erzeuge Buchung vergleichsBuchung {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Vollpension
}
hat Zimmer: zimmer1
hat Gaeste: gast1

Hier sind nun mehrere Dinge zu beachten. Einerseits macht ein Vergleich nur Sinn, wenn man die Einschränkungen der Eigenschaften und Referenzen bei der Abfrage (Finde) verringert. Denn wenn man alle Eigenschaften belegt, also gleich nach dem Objekt sucht, was man haben möchte, so reicht ein einfaches Nicht Null um zu testen, ob das Objekt in der Ergebnismenge existiert.

Eine andere Sache ist, dass man hier gleichermaßen Beinhaltet und Gleich anwenden kann, da man vorher genau überprüft hat, dass es nur ein Element in der Ergebnismenge geben darf. Dies ist auch sinnvoll, denn man hat ja schließlich nach der Buchung für ein bestimmtes Zimmer gefragt und diese darf für den angegebenen Zeitraum nur einmal existieren.

Wichtig ist noch, dass das Vergleichsobjekt hier mit Erzeuge definiert wird. Da wir uns hier in einem Endzustand eines Testfalls befinden, wird das Objekt selbstverständlich nicht mehr in der Datenbank erzeugt. Aber um sich solch eine Möglichkeit nicht zu versperren, könnte man in diesem Fall darüber nachdenken, die DSL so zu definieren, dass Objekte, die nur zur Laufzeit benötigt werden, erstellt werden, indem man das Erzeuge weglässt.

Ich bitte noch zu beachten, dass dieses Beispiel nur dazu dient, die Möglichkeiten zu zeigen. Das ursprüngliche Beispiel von oben ist deutlich kürzer und deshalb vorzuziehen.

Schlussbemerkung

Ich denke, dass diese Lösung mit den verschiedenen Assertions durchaus relativ intuitiv zu benutzen ist. Jedoch könnte man die Assertions, welche extra für einzelne Objekte vorhanden sind, weglassen und stattdessen immer mit Listen arbeiten. Schließlich kann eine Ergebnismenge ja auch eine Liste mit nur einem Element sein. Durch die Assertions zum Überprüfen der Listengrößen hat man wieder die volle Funktionalität.

Momentan bin ich dabei, diese Dinge im Prototyp umzusetzen. Ich möchte in der nächsten Zeit erst einmal soweit kommen, dass ich mit dem in den letzten Posts gezeigten Beispiel den Hibernate-Code generieren kann, der dann die Objekte aus der DSL letztendlich in die Datenbank bringt bzw. wieder abfragt. Wenn die grundlegenden Sachen umgesetzt sind, werde ich einen Post über Schritt 4, die Codegenerierung veröffentlichen.

Montag, 5. Juli 2010

Nachtrag zu Schritt 3 – Hinzufügen eines neuen Use Cases und Erweiterung des Domänen-Modells

In diesem Post möchte ich noch einen kleinen Nachtrag zu dem vorherigen Post vorstellen. Letzte Woche habe ich an Hand des Use Cases “Zimmer buchen” aus der Domäne “Buchungssystem” vorgestellt, wie man in einer DSL einen Akzeptanztest mit Anfangs- und Endzustand in der Datenbank beschreiben kann. Dabei wurde zunächst ein Modell entwickelt, welches die Entitäten aus der Datenbank als Fachmodell beschreibt. In diesem Post werden wir das Modell erweitern, um einen weiteren Use-Case zu unterstützen.

Und zwar soll das zu entwickelnde System die Bewertung von Beherbergungsbetrieben durch den Gast unterstützen. Dabei wollen wir es einfach halten: der Gast schreibt lediglich eine Bewertung in Textform.

Erweiterung des Modells

Aufbauend auf dem Modell aus dem letzten Post gibt es eine neue Klasse “Bewertung”, welche Referenzen auf auf genau einen Gast und einen Beherbergungsbetrieb besitzt.

EcoreDiagUsecase02

Mit dieser Erweiterung ist das Modell bereit für den Testfall. Dieser soll lediglich prüfen, ob die Bewertung erfolgreich erstellt wurde. Dazu wird der folgende Anfangszustand beschrieben:

Import 'Hotel.ecore'

Erzeuge Beherbergungsbetrieb hotel1 {
Betriebsart = Hotel
Bezeichnung = "Hotel am Park"
}

Erzeuge Gast gast1 {
Vorname = "Max"
Nachname = "Mustermann"
Email = "Max@Mustermann.de"
}


Hier wird ein Beherbergungsbetrieb und ein Gast erzeugt. Das zu testende Softwaremodul muss eben für diesen Gast und Beherbergungsbetrieb eine Bewertung erzeugen. Das Ziel des Endzustands ist es nicht, den Inhalt der Bewertung zu überprüfen, sondern, ob diese überhaupt existiert:



Import 'Hotel.ecore'
// benutze Objekte aus dem Anfangszustand
Import 'Bewertung1_before.atdsl'

Finde Bewertung bewertung1
hat Bewertender: gast1
hat Betrieb: hotel1

// Ist die Bewertung vorhanden
Nicht Null? bewertung1


Schlussbemerkung



An Hand des neuen Use Cases konnte man sehen, wie man als Domänen-Experte in der Lage ist, an Hand von Use Cases bzw. deren Testfällen ein Domänen-Modell zu entwickeln. Durch diesen iterativen Vorgang kann sichergestellt werden, dass ein Domänen-Modell ganz genau auf seine Verwendung abgestimmt ist, d.h. das sich im Modell nur die Elemente (Klassen, Relationen) befinden, welche zur Erfüllung der Funktionalität erforderliche sind, anders als es zum Beispiel der Fall wäre, wenn anfangs ein komplettes Modell entwickelt wird.



Ein Fragestellung, die sich in diesem und dem letzten Post ergeben hat, ist, wo man am besten das Domänen-Modell platziert. Die ursprüngliche Idee bestand darin, für jeden Use Case ein Domänen-Modell zu entwickeln, welches nur die Elemente besitzt, um eben diesen Use Case abzubilden. Nun haben wir ein Modell in erweiterter Form für verschiedenen Use Cases verwendet. Ich denke, die Entscheidung, wo es am sinnvollsten ist, ein Domänen-Modell einzusetzen, wird sich erst durch die praktische Verwendung solch eines Systems ergeben. Eventuell ist es aber von Vorteil, dem Benutzer die Möglichkeit zugeben, Domänen-Modelle überall da zu definieren, wo er sie benötigt, sei es für jeden Use Case separat oder global für ein gesamtes Projekt.

Freitag, 2. Juli 2010

Erstellen von Akzeptanztests mit einer DSL (Schritt 3 / Teil 2)

In diesem Post soll nun der Ablauf der Erstellung eines Akzeptanztests beschrieben werden. Dabei wird zuerst anhand des Use Case ein Modell entwickelt. Später wird ein allgemein geltender Ausgangszustand und zwei Testfälle basierend auf dem Modell entwickelt. Anhand dieser Testfälle werden dann die Möglichkeiten und Probleme diskutiert.

Dabei möchte ich hier den Use Case darstellen, den ich bereits im letzten Post vorgestellt habe. Dieser ist nicht sehr umfangreich und deshalb als Beispiel gut geeignet.

Projekt-Struktur des Eclipse-Plugins

Bevor wir zur eigentlichen Umsetzung kommen, möchte ich gerne auf einen sehr praxisbezogenen Punkt eingehen. In dem Prototyp, der gerade in Arbeit ist, habe ich mir eine relativ genau vorgegebene Struktur überlegt, welche das Eclipse-Plugin erzeugt. Dies soll dem Benutzer dabei helfen, Ordnung in seine Use Cases und Akzeptanztest-Fälle zu bringen:

EclipseProjectStruktur01

Dabei wird (über entsprechende Benutzerassistenten) eine Package-ähnliche Ordner-Struktur erzeugt, wie man sie aus Java kennt. Im Package versteckt sich jeweils der Name des Use Case und als Subpackages die Testfälle dieses Use Case. Hierbei muss man auf die Länge der Namen achten, da zu lange Namen nicht als Packages verwendet werden können. Dennoch sollten sie sinnvoll sein (anders als im Bild).

Im Use Case Package befindet sich die Ausgangszustands-Datei, gekennzeichnet durch das “init” im Namen. Alle Dateien mit der Endung “atdsl” sind DSL-Dateien. Die Packages der Test Cases haben jeweils eine Datei für den Anfangs- (before) und den Endzustand (after). (Die Namen wurden in Anlehnung an JUnit gewählt).

Die wichtigen Informationen einerseits für den Use Case, aber auch für die Testfälle befinden sich im jeweiligen Ordner in properties-Dateien. Auf deren Inhalt soll momentan nicht eingegangen werden, dies ist für den Ablauf zunächst nicht wichtig. Außerdem befindet sich das Ganze noch in Entwicklung. Mögliche Properties sind Pfade zu Modell und DSL-Dateien, aber auch zum Beispiel eine Prosa-Beschreibung des Use Case bzw. Testfalls.

Des Weiteren befinden sich alle Dateien, die vom Benutzer editiert werden können bzw. sollen in einem Source-Ordner “src”. Wie man im Bild sehen kann, gibt es einen zweiten Ordner “src-gen”. Dieser hält sich zur Verfügung, um bei der Code-Generierung, welche im Anschluss an die Formulierung der Tests folgen soll, erzeugte Quellcode-Dateien aufzunehmen, beispielsweise vom Ecore-Modell.

Das Domänen-Modell

Der Beispiel-Use Case beschäftigt sich mit der Domäne “Buchungssystem”. Dabei geht es um die Buchung eines Hotel-Zimmers durch einen Gast. Der Benutzer könnte zunächst ein vollständiges Domänen-Modell entwickeln, welches als Basis für die gesamte zu entwickelnde Software gilt. Wir beschreiten hier aber einen anderen Weg: das Domänen-Modell wird zunächst nur soweit entwickelt, wie es für den Use Case notwendig ist. Normalerweise kann so auch jeder Use Case sein eigenes Modell besitzen. Man könnte aber natürlich auch nur ein Modell verwenden und dies mit jedem Use Case erweitern. So oder so bietet diese Vorgehensweise den Vorteil, dass der Benutzer über seine Domäne reflektiert und nur die wirklich benötigten Elemente auswählt. Das Modell wird jeweils in einem iterativen Schritt weiterentwickelt.

Das in diesem Beispiel verwendete Domänen-Modell ist relativ einfach. Daher wird es hier auch keine große Iteration geben. Bei komplexeren Domänen und Use Cases sieht dies selbstverständlich anders aus.

Um den Use Case der Buchung formulieren zu können, benötigt man mindestens folgende Modell-Elemente:

EcoreDiagUsecase01

Es existieren die Entitäten Beherbergungsbetrieb, Zimmer, Gast und Buchung. Eine Buchung hat kann mehrere Zimmer und Gäste haben, ein Beherbergungsbetrieb hat mehrere Zimmer. Die Enumerationen für Betriebsart, Zimmerart usw. sind im Bild der Übersichtlichkeit halber nicht zu sehen.

In unserem Fall reicht dieses Modell schon für beide Testfälle aus.

Ausgangszustand

Der Ausgangszustand dient dazu, einen bestimmten Zustand in der Datenbank herzustellen, unabhängig davon, welcher Testfall gerade läuft. Um eine gewisse Testhygiene zu erreichen, wird er entweder vor oder nach einem Testfall wiederhergestellt. Dabei könnte der Ausgangszustand für die gesamte Domäne gültig sein oder auch nur für einen Use Case. Momentan habe ich dafür entschieden, den Ausgangszustand für jeden Use Case einzeln festzulegen.

Im Ausgangszustand können einige Objekte bereits angelegt werden. Hier beschränken wir uns darauf, sicherzustellen, dass die benötigten Tabellen leer sind. Deshalb werden alle Einträge darauf gelöscht, in dem alle Objekte gelöscht werden:

Import 'model/Hotel.ecore'

Lösche alle Beherbergungsbetrieb
Lösche alle Zimmer
Lösche alle Gast

Erster Testfall – Erfolgreiche Buchung eines Zimmers

Bei einem Buchungssystem für Hotels und andere Beherbergungsbetriebe gibt es eine Besonderheit. Der Gast kann sich meist sein Zimmer nicht genau aussuchen. Er gibt bestimmte Optionen an, wie das er ein Doppelzimmer haben möchte, und bekommt dann ein entsprechendes zugewiesen. Deshalb wird im Anfangszustand nur ein Hotel mit einem Zimmer angelegt.

Anfangszustand in der DSL:

Import '../model/Hotel.ecore'

Definiere Hotel = Beherbergungsbetrieb {
Betriebsart = Hotel
}

Erzeuge def:Hotel Hotel1 {
Bezeichnung = "Hotel am Park"
}
hat ZimmerListe: zimmer1

Erzeuge Zimmer zimmer1 {
Zimmerart = Einzelzimmer
Zimmernummer = "1"
}

Erzeuge Gast gast1 {
Vorname = "Max"
Nachname = "Mustermann"
Email = "Max@Mustermann.de"
}

Im Endzustand wird nach der angelegten Buchung gesucht. Wie bereits im vorherigen Post angedeutet, muss man sich hier darauf verlassen, dass das getestete Softwaremodul bestimmte Standardwerte verwendet, wie hier etwa die Anreise am 1.7. und die Abreise am 5.7., da es so nicht möglich ist, diese Werte als Input zu übergeben.

Endzustand in der DSL:

Import '../model/Hotel.ecore'
// benutze Objekte aus dem Anfangszustand
Import 'testcase1_before.atdsl'

Finde Buchung buchung1 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Vollpension
}
hat Zimmer: zimmer1
hat Gaeste: gast1

// Buchung muss vorhanden sein
Nicht Null? buchung1

Sollte die Buchung nicht gefunden werden, so ist die Variable am Ende null. Das heißt, es muss nun überprüft werden, ob die Buchung “Nicht null” ist. Ist das Ergebnis der Prüfung true, dann ist der Test erfolgreich.

Wie im Beispiel zu sehen ist, verwendet die DSL-Datei des Endzustands die des Anfangszustandes, um die da erzeugten Objekte zu übernehmen. Ein anderer Weg wäre, hier noch einmal explizit nach dem angelegten Zimmer und dem Gast zu suchen.

Zweiter Testfall – Doppelte Buchung

Im zweiten Testfall werden im Anfangszustand prinzipiell die selben Objekte erzeugt. Außerdem wird bereits eine Buchung des Zimmers durch einen zweiten Gast erzeugt. So sollte das Zimmer nicht mehr belegt werden können.

Anfangszustand:

Import '../model/Hotel.ecore'

Erzeuge Buchung buchung1 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Halbpension
}
hat Zimmer: zimmer1
hat Gaeste: gast2

Definiere Hotel = Beherbergungsbetrieb {
Betriebsart = Hotel
}

Erzeuge def:Hotel Hotel1 {
Bezeichnung = "Hotel am Park"
} hat ZimmerListe: zimmer1

Erzeuge Zimmer zimmer1 {
Zimmerart = Einzelzimmer
Zimmernummer = "1"
}

Erzeuge Gast gast1 {
Vorname = "Max"
Nachname = "Mustermann"
Email = "Max@Mustermann.de"
}

Erzeuge Gast gast2 {
Vorname = "Moritz"
Nachname = "Meier"
Email = "Moritz@Meier.de"
}

Wie man hier sieht, befindet sich die Buchung im selben Zeitraum wie die, die durch die zu testende Software angelegt werden soll. Im Endzustand wird nun zunächst nach der angelegten Buchung gesucht. Diese muss natürlich noch vorhanden sein und darf z.B. nicht überschrieben worden sein. Sucht man hingegen nach der Buchung, die eigentlich nun hätte angelegt werden sollen, so darf diese natürlich nicht vorhanden sein.

Endzustand:

Import '../model/Hotel.ecore'
// benutze Objekte aus dem Anfangszustand
Import 'testcase2_before.atdsl'

Finde Buchung buchung1 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Halbpension
}
hat Zimmer: zimmer1
hat Gaeste: gast2

// Buchung muss vorhanden sein
Nicht Null? buchung1

Finde Buchung buchung2 {
Anreisedatum = 01.07.2010
Abreisedatum = 05.07.2010
Leistungsart = Vollpension
}
hat Zimmer: zimmer1
hat Gaeste: gast1

// Buchung darf nicht vorganden sein
Null? buchung2

So wird nun abgefragt, ob die entsprechende Variable null ist.

Eine weitere Möglichkeit wäre, die Suche einzuschränken und beispielsweise nur nach einer Buchung im besagten Zeitraum und für dieses spezielle Zimmer zu suchen, also unabhängig vom Gast. Sollten mehrere Buchungen existieren, würde hier eine Liste zurückgegeben werden. (Wie die DSL mit Ergebnismengen, die mehr als ein Objekt beinhalten, umgehen muss, darüber werde ich einen eigenen Post schreiben.) Man könnte nun überprüfen, ob die Liste nun mehrere Buchungen beinhaltet oder nur die eine, die schon vorher angelegt wurde. Dies wäre ein anderer Weg, den Testfall zu überprüfen.

Bemerkungen zu diesem Beispiel

Wie man in den beiden Testfällen sehen konnte, so sind die im Anfangszustand erzeugten Objekte fast gleich. Da liegt die Idee nahe, einfach bereits im Ausgangszustand diese Objekte zu erzeugen. Dies kann man durchaus machen, abhängig davon, wie oft und in wievielen Testfällen die Objekte gleichermaßen verwendet werden. Wie bereits angedeutet spielt hier auch eine Rolle, ob man einen Ausgangszustand je Use Case definiert, oder ob es für das gesamte zu testende System einen einzigen Ausgangszustand gibt. Dazu kommt noch, dass man eventuell einen sauberen Zustand der Datenbank bewahren will und die Objekte nicht behalten möchte, obwohl sie in jedem Testfall verwendet werden.

Dieses Beispiel ist leider nicht so umfangreich, um alle Möglichkeiten zu zeigen. Eventuell werde ich in einem der nächsten Posts das Beispiel wieder aufgreifen und erweitern.

Desweiteren sollte dieses Beispiel die grundlegende Idee der Beschreibung von Akzeptanztests in einer DSL wiedergeben. Hier wird sicher noch einige Arbeit investiert werden, um die DSL noch besser und den Prozess und die Struktur des Projektes optimaler zu gestalten.