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.

Mittwoch, 30. Juni 2010

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

In diesem Post und dem nächsten möchte ich beschreiben, wie Akzeptanztests in einer DSL formuliert werden könnten und wie der Ablauf zum Beschreiben dieser Tests aussieht. Außerdem werde ich auf mögliche Probleme eingehen.

Dazu werde ich einen kleinen Use Case als Beispiel heranziehen und auf Basis dessen mehrere Testfälle beschreiben. Erst einmal sind die Eigenschaften der DSL und ein paar allgemeine Informationen an der Reihe. Den kompletten Modellierungs- bzw. Testbeschreibungsablauf werde ich im nächsten Post durchgehen.

Grundlegende Aktionen in der DSL

Zunächst einmal geht es in dem System, welches als Ziel dieses Blogs entwickelt werden soll, um das Testen von datenbankbasierten Applikationen. Deshalb liegt zunächst die Idee nah, Datenbankabfragen bzw. – manipulationen durchzuführen, welche den Anfangs- und Endzustand eines Akzeptanztests beschreiben.

Grundsätzlich erzeugt man anfangs bestimmte Daten in der Datenbank bzw. überprüft einen bestimmten Anfangszustand, dann führt man die zu testende Funktion durch und letztendlich überprüft man gezielt bestimmte Tabellen in der Datenbank, ob die getestete Funktion auch ihr Ziel erfüllt hat.

Um dies durchzuführen, benötigt man bestimmte Aktionen, welche das System implementiert. Ähnlich wie in SQL gibt es eine Art “Insert-Befehl” zum Erzeugen und anschließendem Einfügen von Daten und einen “Select”-Befehl zum Selektieren der Daten. Außerdem sollte es eine Aktion zum Überprüfen von Daten geben, ähnlich einer Assertion, wie man sie aus verschiedenen Programmiersprachen kennt, damit man beispielsweise Objekte auf Gleichheit oder Ähnliches prüfen kann. Schlägt so eine Assertion fehl, so ist auch der Testfall negativ ausgefallen.

Im folgenden sind ein paar Beispiele aufgeführt, welche ich direkt aus dem momentan von mir entwickelten Prototyp entnommen habe. Die Entwicklung der DSL an sich ist noch nicht abgeschlossen, deshalb kann sich die Notation noch ändern.

Beispiel für Erzeugung

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

Erzeuge Zimmer zimmer2 {
Zimmernummer = "2"
Zimmerart = Doppelzimmer
}

Erzeuge Beherbergungsbetrieb betrieb1 {
Betriebsart = Hotel
Bezeichnung = "Hotel am Park"
}
hat ZimmerListe : zimmer1 zimmer2



Durch das Schlüsselwort “Erzeuge” wird die Erzeugung eines Objektes des direkt dahinter folgenden Typs (bzw. EClass aus dem Domänen-Modell) eingeleitet. Das Objekt kann einen optionalen Namen haben (wie hier zimmer1, zimmer2 usw.), welcher für Referenzen verwendet wird. Nachfolgend sind die in Klammern eingeschlossenen Eigenschaften vermerkt. Besitzt die Klasse Referenzen so sind diese am Ende jeweils mit einem “hat” eingeleitet, gefolgt von dem Namen der Referenz und einer liste der jeweiligen Objekte.



Beispiel für Selektieren/Finden von Daten




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

Finde Gast gast1 {
Vorname = ""
Nachname = ""
Email = ""
}

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



Das Selektieren von Daten ähnelt sehr dem Erzeugen. Der Unterschied ist, dass hier die einzelnen Eigenschaften und Referenzen ähnlich einer WHERE-Clause in SQL verwendet werden, um das Objekt in der Datenbank zu selektieren und unter dem angegebenen Variablennamen zu speichern. Das Objekt kann demnach auch null sein, wenn die Ergebnismenge leer ist, oder auch eine ganze Liste von Objekte beinhalten. Wie das System damit umgehen muss, dazu ebenso im nächsten Post mehr.



Im obigen Beispiel wird eine Buchung aus der Datenbank selektiert, welche auch auf andere Objekte verweist. Diese müssen vorher ebenfalls gefunden werden. Dadurch wird sichergestellt, dass das gewünschte Zimmer und der Gast auch in der Datenbank existiert. Es sollte aber auch möglich sein, ein bereits vorher angelegtes Objekt hier als Referenz einzufügen (dazu ebenso bald mehr).



Beispiel für Assertions




// Buchung muss vorhanden sein
Nicht Null? buchung1



Eine Assertion oder Zusicherung überprüft den Zustand eines bestimmten Objektes und beendet den Test im Fehlerfall. Typische Assertions wären zum Beispiel der Vergleich zweier Objekte oder die Überprüfung, ob ein Objekt null ist.



Dies ist auch im obigen Beispiel der Fall. Greift man das Selektieren der Buchung von weiter oben auf, so wäre eine logische Folge zu überprüfen, ob die Buchung eine leere Menge ist oder nicht.



Da die DSL eher einer natürlichen Sprache ähneln soll anstatt einer Programmiersprache, werden die entsprechenden Sprachfunktionen nicht mit “AssertNotNull” oder ähnlichem bezeichnet, sondern direkt mit Wörter bzw. Wortgruppen wie “Null?”, “Nicht Null?” oder zum Beispiel “Gleich?”. Danach folgen ein oder mehrere Objekte als Parameter.



Ein vollständiges Beispiel aus der DSL werde ich im nächsten Post vorstellen. Des weiteren soll die DSL noch einfacher und leichter lesbar werden, sodass sie mehr einer natürlichen Sprache ähnelt.



Der Use Case



Um einen leichter verständlichen Einstieg in die Verwendung der DSL zu finden, verwenden wir einen einfach Use-Case aus der “Buchungssystem”-Domäne. Der Use Case ist: ein (registierter) Gast bucht ein Zimmer in einem bestimmten Hotel. Im Prinzip kann man diesen Use Case unterteilen, denn bevor der Gast ein Zimmer bucht, würde er zunächst nach einem Hotel suchen und die Verfügbarkeit eines gewünschten Zimmers prüfen. Wir gehen einfach davon aus, dass dies bereits passiert ist. Aus dem Use Case lassen sich zunächst zwei Testfälle erzeugen:




  • die Buchung wird erfolgreich durchgeführt: es muss überprüft werden, ob die Buchung auch tatsächlich im System erzeugt wurde


  • es wird ein bereits belegtes Zimmer gebucht: die Buchung darf nicht doppelt eingetragen werden



Der zweite Testfall sollte eigentlich nicht nötig sein, wenn vorher die Verfügbarkeit überprüft wurde. Dennoch könnte es zum Beispiel der Fall sein, dass das letzte verfügbare Zimmer belegt worden ist, bevor der Gast den Buchungsvorgang durchgeführt hat.



Es gibt eventuell noch mehr Testfälle, welche man für diesen Use Case überprüfen kann. Jedoch sollen diese beiden vorgestellten für die Beschreibung der Testerstellung ausreichen.



Eine Einschränkung im Voraus – Input-/Output-Problem



Sieht man sich die Testfälle an, so erkennt man relativ schnell eine Schwachstelle in dem System, wie wir es bisher beschrieben haben. Nicht alle Ein- und Ausgaben einer zu testenden Software lassen sich über die Datenbank realisieren. Während ich weiter oben geschrieben habe, dass es grundsätzlich um das Erzeugen von Daten in der Datenbank im Anfangszustand und das Überprüfen von Daten im Endzustand geht, so muss ich hier einige zusätzliche Bemerkungen dazu loswerden.



Stellt man sich einmal praktisch eine Buchungssystem für Hotels vor, welches über ein Web-Frontend bedient wird, so werden die Eingaben beim Buchen eines Hotelzimmers direkt über ein Userinterface eingegeben, natürlich nicht über die Datenbank. Der Gast an sich ist registriert und befindet sich in der Datenbank, ebenso wie Beherbergungsbetriebe und Zimmer.



Dasselbe Problem hat man bei bestimmten Use Cases auch beim Output der zu testenden Software. Wie soll das System nun damit umgehen? Es müsste in der Lage sein, nicht nur Zustände in der Datenbank zu beschreiben, sondern auch Ein- und Ausgabedaten.



Dabei könnten standardisierte Schnittstellen angeboten werden, welche auch die zu testende Software implementieren kann, wie die Standard-Ein- und Ausgabe, XML-Dokumente oder ähnliches. Dies ist ein relativ komplexes Thema für sich, welches wir in einem separaten Post behandeln werden. Da eine Umsetzung momentan zu umständlich wäre, beschränken wir uns auf Use-Cases, wo man tatsächlich an Hand der Datenbank Tests durchführen kann.



Speziell für den oben beschriebenen Use Case wird es so sein, dass wir vorher festgelegte Standard-Ein und Ausgaben annehmen, welche letztendlich überprüft werden. Das System betrachtet also diese Daten nicht, sie müssten stattdessen fest in der zu testenden Software implementiert sein.



Ablauf der Modellierung/Testformulierung… nächstes Mal



Auf Grund des Umfangs werde ich eine vollständige Beschreibung des Prozesses zur Beschreibung des Use Cases im nächsten Post nachliefern.



Wie sich herausgestellt hat, so gibt es noch einige Schwierigkeiten bei der Umsetzung des Systems. Dabei muss so ein Problem wie die Übergabe von Eingabeparametern und das Empfangen der Ausgabe aus dem aufgerufenen Softwaremodul zunächst abstrakt behandelt werden. Jedoch soll es dazu noch eine ausführliche Diskussion geben, wie sich diese Problemstellung in das System integrieren lässt.



Außerdem sind noch einige Arbeiten an der DSL selbst nötig, um deren Verwendung für den Benutzer möglichst einfach zu gestalten. Dennoch soll der nächste Post die Verwendung des Systems im Bezug auf das Schreiben eines Tests inklusive Domänen-Modell ausführlich beschreiben, um die grundsätzliche Arbeitsweise des Systems darzustellen.

Dienstag, 22. Juni 2010

Mapping des Domänen-Modells zur Datenbank (Schritt 2)

Dieser Beitrag soll sich mit der Zusammenarbeit zwischen der fachlichen Sicht, also im Prinzip dem Domänen-Modell, und der technischen Sicht auf die Domäne, sprich der Datenbank, beschäftigen.

Da sich in diesen beiden verschiedenen Welten je nach Komplexität der Domäne einige Unterschiede ergeben können, ist es nötig, ein Mapping zwischen dem Domänen-Modell, welches der Benutzer des Systems entworfen hat und einer bereits existierenden Datenbank vorzunehmen. Dieser Vorgang müsste durch einen Entwickler vorgenommen werden, welcher Kenntnis von der Datenbank besitzt, aber auch mit dem Domänen-Modell vertraut ist.

Mapping – Zunächst nur Theorie

Im Folgenden möchte ich nicht auf eine mögliche Implementierung bzw. Umsetzung solch eines Mappings eingehen, wie es in den anderen Schritten durchaus der Fall sein wird. Aus verschiedenen Gründen werde ich mich zunächst auf die Domänen-Modellierung und den daraus resultierenden Ablauf innerhalb des Systems konzentrieren. Die nächsten Schritte (Beschreibung des Akzeptanztests in einer DSL, Codegenerierung usw.) gehören dazu und werden auch in naher Zukunft prototypisch umgesetzt. Das Mapping auf die Datenbank wird zunächst nur abstrakt behandelt.

Warum muss das Mapping sein?

Wenn man sich mit dieser Frage beschäftigt, muss man zunächst die potentiellen Unterschiede zwischen dem Domänen-Modell und dem dazugehörigen Datenbank-Schema erkennen. Zunächst ist es so, dass Entitäten einer Datenbank bereits eine Entsprechung in der Objekt-Welt haben. Dabei gilt: eine Tabelle entspricht einer Klasse. Zur Vereinfachung nehmen wir einfach an, dies wäre so. Um weiter in der Domäne “Buchungssystem” zu bleiben, gibt es also für die Klasse Beherbergungsbetrieb eine Tabelle in der Datenbank, sowie für Zimmer, Buchung usw.

Der Unterschied findet sich meistens in den Relationen zwischen den Klassen. Das Problem sind dabei weniger die One-To-Many Relationen, wie zum Beispiel zwischen Beherbergungsbetrieb und Zimmer. Diese kann ein OR-Mapper beispielsweise ganz gut allein auflösen, wobei man auch hier eventuell Hand anlegen muss, da vorhandene Felder in der Datenbank anders benannt sind, also dies im Domänen-Modell der Fall ist. Schwieriger wird es bei Many-To-Many Beziehungen. Im folgenden Bild sieht man jeweils Ausschnitte aus einmal dem Domänen-Modell und dem ERD als Grundlage für eine Datenbank:

MappingBeide01

Wir schauen uns hier die Beziehung zwischen Buchung und Zimmer an. Es handelt sich um eine Many-To-Many Beziehung – in einer Buchung können mehrere Zimmer gebucht worden sein und ein Zimmer kann selbstverständlich auch mehrere Buchungen haben.

Zunächst sieht man, dass die Beziehungen dennoch sehr unterschiedlich aussehen. Im Domänen-Modell gibt es eine eher einseitige Beziehung. Die Buchung hat eine oder mehrere Zimmer, im Bild durch einen Pfeil dargestellt. Im Modell selbst hätte die Klasse Buchung dann eine Member-Variable (vom Typ EReference), welche eine Liste von Zimmer-Objekten darstellt. Die einseitige Sichtweise, hier von Buchung aus gesehen, entsteht aus der Sichtweise des Domänenexperten: für ihn ist es hier nur interessant, dass eine Buchung eine Liste von Zimmern haben kann, dass ein Zimmer auch von mehreren Buchungen referenziert sein kann, ergibt sich von allein. (Hier kommt natürlich dazu, dass jedes Zimmer auch eine Liste seiner Buchungen haben könnte und so eine zweite, entgegengesetzte Referenz existieren könnte. Aber dies wäre hier im Beispiel nicht gewünscht. Jedoch sollte die Möglichkeit nicht unerwähnt bleiben, denn sie ist in Ecore durchaus möglich.)

In der Datenbank kann die Tabelle Buchung nicht einfach eine Liste mit Zimmer beinhalten. Hier benötigt man eine dritte, sogenannte Join-Tabelle, welche Foreign Keys auf die beiden anderen Tabellen besitzt.

Aufgaben des Mappings

Da es im Domänen-Modell keine Join-Tabelle gibt, muss irgendwo definiert sein, dass die Many-To-Many-Beziehung zwischen Zimmer und Buchung über diese Tabelle geführt wird. Daher noch einmal eine Übersicht, was das Mapping leisten muss:

  • Zuordnung je einer Tabelle zu einer Klasse
  • Zuordnung der Spalten in den Tabellen zu Attributen
  • Zuordnung der Foreign Keys zu Referenzen im Modell (bei One-To-Many)
  • Bei Many-To-Many-Beziehungen Zuordnung der Join-Tabelle

Bei den Attributen in den Klassen ist noch zu beachten, dass es auch Attribute gibt, welche auf Enumerationen verweisen. Enumerationen wären aber im DB-Schema wiederum Tabellen, also muss man hier auch auf diese verweisen und kann nicht einfach den Wert eintragen.

Problem: zu große Unterschiede Modell – Datenbank

Bis jetzt haben wir angenommen, es gibt für jede Klasse im Modell grundsätzlich eine Tabelle in der Datenbank. Was passiert aber, wenn dies nicht der Fall ist, und es in der Datenbank bestimmte Tabellen nicht gibt, oder es im Modell Klassen gibt, welche man aber nicht auf eine Tabelle abbilden kann.

Nun könnte man argumentieren, dass der ursprüngliche Datenbankentwurf sich auch in irgendeiner Weise an ein Fachklassenmodell halten musste und es so doch relativ große Übereinstimmungen geben muss. Sollte dies nicht der Fall sein, so kann man in Betracht ziehen, dass Model, welches man für seine Testfälle entwirft, als Grundlage für solch eine Datenbank heranzuziehen. (Dies könnte auch ein Use-Case für das System an sich sein. Der Domänen-Experte arbeitet von Anfang an der Software mit, indem er ein Modell bereitstellt, welches sich aus den Testfällen heraus ergeben hat und so nur die Elemente enthalten dürfte, die wichtig für die eigentliche Funktionalität sind.)

Der andere Weg wäre, das Modell entsprechend anzupassen, was aber schwierig wäre, da es hier wiederum einen größeren Dialog zwischen Domänen- und Datenbankexperten geben müsste.

Wenn es im Modell Klassen geben sollte, welche man nicht auf Tabellen abbilden kann, so sollte man in Betracht ziehen, das Modell nicht zu konkret zu gestalten. Statt dessen kann man durch Definitionen Spezialisierungen bilden, welche wiederum auf das allgemeinere Domänen-Modell abgebildet werden und so nicht mit der Datenbank in Berührung kommen.

Fazit

Ich hoffe, dieser kleine Einstieg in den relativ komplexen Schritt des Mappings ein bisschen Klarheit bringen konnte, wieso diese Funktion notwendig ist. Schon wegen diese Komplexität wird die Umsetzung des Prototyps sich darauf beschränken, keine vorhandenen Datenbank zu benutzen, sondern diese selbst aus dem Modell erstellen. Dies ist beispielsweise mit Hibernate ohne Probleme möglich.

Wie das Mapping im Genauen aussieht und welche Probleme noch entstehen können, würde man erst bei einer entsprechenden Umsetzung betrachten können. Letztendlich ist ein Austausch zwischen dem Domänen-Experten und dem Entwickler als Experte für die Datenbank sinnvoll, wenn nicht sogar dringend notwendig, um von vornherein eine Synchronität zwischen der fachlichen und der technischen Ebene herzustellen.

Mittwoch, 16. Juni 2010

Erstellen eines Domänen-Modells für Akzeptanztests (Schritt 1)

Im letzten Post habe ich kurz die verschiedenen Schritte vorgestellt, die ein System zur Akzeptanztestformulierung benötigt. Heute werde ich mich ausführlicher mit dem ersten Schritt beschäftigen – dem Erstellen eines Domänen-Modells.

Zunächst einmal wäre abzuschätzen, welche Elemente solch ein Modell benötigt und wie die Erstellung dessen ablaufen soll.

Wie müsste das Modell aufgebaut sein?

Zunächst einmal sollen alle Modelle, welche vom Benutzer erstellt werden können, die selben Elemente verwenden. Außerdem soll das System beliebige Modelle verarbeiten können. Daher benötigt es eine Art Meta-Modell, welches bereits Strukturen bietet, um eine “Objekt-Welt”, von der wir hier ausgehen, darzustellen.

Etwas grob beschrieben braucht man im Meta-Modell bestimmte Klassen, deren Eigenschaften und Beziehungen zwischen diesen Klassen. Wenn man so etwas beschreiben möchte, kommt man sehr schnell in Richtung objektorientiertes Design und UML. Dies wäre natürlich für den Zweck des Systems zu viel, deshalb benötigen wir etwas kleineres, einfacheres.

Wie soll die Erstellung eines Modells aussehen?

Der Benutzer möchte sein Modell natürlich nicht in Textform beschreiben, sondern er benötigt einen komfortablen Editor, wenn möglich, sogar in grafischer Form. Mit solch einem Editor sollte es möglich sein, einzelne Klassen mit ihren jeweiligen Eigenschaften zu erstellen. Beziehungen zwischen ihnen werden am besten durch Verbindungen und Pfeile, also rein grafisch angezeigt. Der Benutzer sollte in der Lage sein, sich die Klassen seines Modells beliebig anzuordnen, um so den gewünschten Überblick über sein Modell zu bekommen.

Vorschlag für ein Meta-Modell: Eclipse EMF (Ecore)

Das in EMF eingebaute Ecore-Metamodell ist genau das, was wir suchen. Es basiert auf Meta Object Facility (MOF), einer von der OMG eingeführten Metadaten-Architektur, bzw. auf deren Untermenge EMOF (Essential MOF). Ecore-Dateien an sich sind in einer XML-Sprache geschrieben, mit der man sich aber nicht auseinandersetzen muss. EMF bietet dazu ein Art Baumeditor an, mit welchem die Bearbeitung relativ komfortabel vonstatten geht. Besser noch ist die Bearbeitung über das Ecore-Diagramm. Dabei wird eine separate Datei angelegt, welche das Diagramm beinhaltet, Änderungen wirken sich aber auch auf die Ecore-Datei aus und anders herum. Den Ecore-Diagramm-Editor, welchen ich im folgenden noch beschreiben werden, ist aus dem GMF (Graphical Modeling Framework). Es gibt noch einen, womöglich besseren, Editor aus dem Ecore Tools Projekt, mit welchem ich mich selbst noch nicht beschäftigt habe.

Im folgenden werde ich kurz die Erstellung eines Ecore-Diagrammes erläutern. Dabei werde ich hauptsächlich auf den Aufbau des Modells eingehen, ich gehe davon aus, dass jeder Interessierte den Assistenten zum Erzeugen der Ecore-Datei findet (EMF muss installiert sein). Die Verwendung des Diagramm-Editors ist außerdem auch recht einfach.

Elemente des Ecore-Metamodells

Um ein Domänen-Modell zu erstellen, verwenden wir nur bestimmte Elemente aus dem Ecore-Metamodell.

  • EPackage: sozusagen ein Container für andere Elemente. Theoretisch kann man mehrere Packages in seinem Modell besitzen, jedoch reicht eines aus. Für den Benutzer nicht weiter wichtig, so wird es intern im System verwendet, um das Modell zu identifizieren. Das EPackage für das in diesem Post vorgestellte Beispiel bekommt den Namen “Hotel”.
  • EClass: eine Klasse aus der Domäne, kann bestimmte Eigenschaften besitzen (siehe EAttribute).
  • EAttribute: stellt eine Eigenschaft einer Klasse dar, wie beispielsweise den Namen eines Gastes. EAttributes können einen von Ecore vorgegebenen Datentyp haben, wie zum Beispiel EString, aber auch ein EEnumLiteral, also einen Enumerationswert
  • EReference: stellt eine Beziehung zwischen zwei oder mehreren Klassen her

Erstellen eines Modells

Zunächst empfiehlt es sich, den Standard Ecore Editor von EMF zu öffnen. Öffnet man den obersten Knoten (“platform:/…”), so befindet sich darin ein leeres EPackage. Dieses sollte nun einen Namen bekommen sowie ein Namespace Prefix und eine URI. Im Screenshot sieht man die beispielhaften Eingaben für die Hotel-Domäne.

Nun könnte man in diesem Editor weiterarbeiten, aber eigentlich möchten wir ja den grafischen Editor benutzen. Mit Rechtsklick auf die Ecore-Datei im Package-Explorer und der Option “Initialize ecore_diagram diagram file” erzeugt man ein entsprechendes Diagramm. Hier findet man auch den Grund, warum man zunächst das EPackage anderweitig anlegen muss: das Diagramm benötigt ein Root-Element.

Anlegen von Klassen

Der Diagramm-Editor bietet am rechten Rand eine Palette von möglichen Elementen an. Zum Erzeugen eines Domänen-Modells ist erst einmal nur EClass, EAttribute und eventuelle EEnum wichtig, außerdem noch die verschiedenen EReference-Elemente Association, Generalization und Aggregation.

Nun kann man Klassen mit ihren jeweiligen Eigenschaften anlegen. Ich habe dies im folgenden Beispiel für die Beispieldomäne durchgeführt.

EcoreDiag01

Dies ist natürlich nur ein Teil der Domäne, dieser soll aber im Moment ausreichen. Es gibt hier mehrere Enumerationen, welche von den Klassen als Datentypen für bestimmte EAttributes verwendet werden. Eine Besonderheit ist, was man im Screenshot jedoch nicht sehen kann, dass das EAttribute “Leistungsarten” der Klasse “Beherbergungsbetrieb” mehrere Werte annehmen kann (also eine Liste ist).

Erzeugen von Relationen

Relationen bzw. EReferences unterscheiden sich im eigentlichen Ecore-Modell und im Diagramm. Im Diagramm gibt es eine Verbindung zwischen den Klassen, um die Referenzen grafisch deutlich zu machen. Im eigentlichen Modell ähneln sie eher den EAttributes, mit dem Unterschied, das sie als Typ eine andere EClass besitzen.

EcoreDiag02

Im Diagramm gibt es drei verschiedenen Typen: Association, Aggregation und Generalization. Dabei wird nur bei Association und Aggregation eine EReference angelegt. Mit Generalization wird eine Vererbung ermöglicht. Dabei wird in der erbenden Klasse das ESuper Types Property gesetzt.

Eine Association ist eine einfache Referenz von einer Klasse zur anderen. So ist eine Buchung immer mit einem Gast assoziiert sowie mit einem Beherbergungsbetrieb. Eine Aggregation hingegen erzeugt eine EReference als Containment-Beziehung. Dies bedeutet, Klasse A beinhaltet sozusagen eine oder mehrere Klassen B. Bei Hotel und Zimmer lässt sich dies gut nachvollziehen.

Ebenso wie bei Attributen kann man bei Referenzen die Anzahl der Elemente angeben. Eine Buchung aus dem oberen Beispiel muss einen Gast und einen Betrieb haben, ein Betrieb aber kann 0 oder viele Zimmer haben. So lassen sich übrigens auch optionale Referenzen und Attribute erschaffen.

Weiterführende Informationen und einen Überblick über die Ecore-Modellierung findet man auch unter Eclipse Modeling Framework (EMF) – Tutorial. Hier zwar mit dem Ecore Tools Editor, das Prinzip ist aber gleich.

Ich hoffe, ich konnte die Grundlagen zum Ecore-Metamodell zum größten Teil näherbringen. Dies wird nicht der letzte Post sein, der sich mit diesem Thema beschäftigt oder einen Bezug dazu hat. Der nächste Post wird sich erst einmal mit dem nächsten Schritt zur Akzeptanztestbeschreibung beschäftigen – dem Mapping eines (Ecore-)Modells zu einer vorhandenen Datenbank.

Dienstag, 15. Juni 2010

Schritt für Schritt zum Akzeptanztest

In diesem Beitrag möchte ich die grundliegenden Schritte zur Erstellung eines Akzeptanztests vorstellen, wie sie in dem zu entwickelnden System umgesetzt werden sollen. Dies soll dazu dienen, gewissermaßen den roten Faden durch das System deutlich zu machen, bevor jeder dieser Schritte implementiert werden kann. Ich erhoffe mir dabei, somit die Kernaufgaben des Systems herauszuarbeiten sowie mögliche Probleme, die dabei entstehen könnten, einzugrenzen.

Nach dieser kurzen Einführung und Zusammenfassung werde ich jeden Schritt jeweils in einem eigenen Post genauer beschreiben. Dazu gehören jeweils die Voraussetzungen, die vorhanden sein müssen, die beteiligen Akteure, sowie die Ergebnisse, welche am Ende des jeweiligen Bearbeitungsschritts herauskommen sollen. Außerdem werden ich in jedes Mal auf die praktische Umsetzung eingehen, insofern ich bereits Ideen dazu habe. Dadurch kann man eventuell bereits erste Einblicke in die spätere Verwendung des Systems erlangen.

Dazu werde ich mir einen konkreten Use-Case in der Beispiel-Domäne “Buchungssystem” überlegen, welchen ich dann beispielhaft schrittweise in jedem Post weiter bearbeiten möchte.

Im Folgenden möchte ich die verschiedenen Schritte kurz vorstellen und erläutern.

  1. Erstellen eines Domänen-Modells

    Als Grundlage für einen Akzeptanztest muss der Benutzer des Systems, in erster Linie ein Analyst, ein Modell seiner Domäne erstellen. Es wird davon ausgegangen, dass er zumindest die Fachklassen seiner Domäne kennt, welche er auch in das Modell umsetzen muss. Er benötigt keine Kenntnis der Datenbank. Außerdem muss das Modell nicht im vollem Umfang die Domäne darstellen, sondern kann auch nur eine Teildomäne abbilden, welche für den jeweiligen Akzeptanztest notwendig ist.

  2. Mapping des Domänen-Modells zur Datenbank

    Nachdem der Analyst die fachliche Sicht auf die Domäne in Form eines Modells dargestellt hat, muss diese in die technische Sicht der Datenbank überführt werden. Diesen Schritt, das Mapping des Modells auf die Datenbank, muss ein Entwickler übernehmen, der Kenntnis von der Datenbankstruktur besitzt.

    In diesem Schritt wird es nötig sein, das Entwickler und Analyst in einen Dialog eintreten und das Domänenmodell gegebenenfalls nochmals angepasst werden muss, um ein genaues Mapping zu ermöglichen. Dies wird jedoch auch von der Komplexität der Domäne abhängig sein.

  3. Erstellen von Akzeptanztests in einer entsprechenden DSL 

    Der Analyst könnte diesen Schritt bereits vor oder parallel zum vorherigen durchführen, denn nachdem er das Domänen-Modell beschrieben hat, wäre er in der Lage dazu. Jedoch ist es angeraten, erst das Mapping auf die Datenbank abzuwarten, falls dies noch zu Änderungen im Modell führt.

    Nun hat der Benutzer verschiedene Möglichkeiten.

    1. Definitionen

      Definitionen sind Konstrukte in der DSL, welche aufbauend auf dem Domänen-Modell die Beschreibung von häufig wiederverwendbaren Standardobjekten ermöglichen. Verwendet der Benutzer für mehrere Akzeptanztests, die auf dem gleichen Modell basieren, bestimmte Objekte aus seiner Domäne, die immer wieder gleich sind, oder die zum Beispiel bestimmte Standardwerte besitzen, welche im Akzeptanztest an sich irrelevant sind, so kann er diese durch Definitionen einmal erstellen und stets wieder verwenden.

    2. Akzeptanztests

      Hier handelt es sich um die Akzeptanztests an sich. Die DSL bietet hierbei Sprachkonstrukte für die Beschreibung von Daten innerhalb eines Anfangszustandes und eines Endzustandes an. Es ist möglich, Objekte der Datentypen aus dem Modell zu erzeugen, aber auch zu überprüfen, ob sie bereits existieren. Der typische Anwendungsfall ist es, im Anfangszustand eine Reihe von Objekten zu erzeugen und im Endzustand abzufragen, ob sich bestimmte Werte geändert haben, oder ob neue Objekte hinzugekommen sind, je nach zu überprüfender Anforderung.

      An sich wird es möglich sein, Definitionen und Akzeptanztests gemischt in einer Datei zu verarbeiten, sie werden ja schließlich in derselben DSL beschrieben. Jedoch empfiehlt es sich, Definitionen und Akzeptanztests getrennt zu beschreiben, um vor allem bei vielen Akzeptanztests eine gewisse Ordnung zu erhalten.

  4. Durchführen der Code-Generierung

    Dieser Schritt erzeugt aus den in der DSL beschriebenen Daten ausführbaren Code, welcher die vorherbestimmten Aktionen auf der Datenbank ausführt. Er sollte vollautomatisch ausgeführt werten, wobei der Vorgang nur vom Benutzer angestoßen wird. Ein nachträgliches Anpassen des Codes sollte nicht nötig sein.

    Hier muss zunächst Modell-Code generiert werden, das heißt Klassen, welche derer entsprechen, die im Modell angelegt wurden. Nachher wird aus jedem Insert und Select aus der DSL für den eingesetzten OR-Mapper spezifischer Code erzeugt, welcher sich der Klassen aus dem Modell bedient. Hierbei wird das Mapping verwendet, welches Anfangs vom Benutzer bzw. Entwickler erstellt wurde.

  5. Durchführen des Akzeptanztests

    Prinzipiell werden in diesem letzten Schritt die aus dem vorherigen erzeugten Artefakte gestartet. Dabei sollte es möglich sein, kompilierte Dateien zu verwenden (z.B. Jar-Dateien) oder auch reinen Quellcode. Dieser würde dann zur Laufzeit übersetzt werden.

    Wichtig hier ist, dass die Ausführung des Tests so gestaltet sein sollte, dass die einzelnen Phasen völlig unabhängig voneinander funktionieren. Dies ist zum einen der Anfangszustand, welcher über die DSL beschrieben und nun ausgeführt werden soll, zum anderen der Endzustand. Dazwischen sollte es Platz für einen oder mehrere Funktionsaufrufe geben, welche die eigentliche Funktionalität zum Erreichen der Anforderung darstellen, für welche der Akzeptanztest geschrieben wurde.

Ich denke, dies sollte als grobe Zusammenfassung über die verschiedenen Schritte ausreichen. Weitaus ausführlicher möchte ich mich mit jedem einzelnen in jeweils einem separaten Post auseinandersetzen.

Eine Idee noch zum Schluss: eventuell kann man den vierten und fünften Schritt zusammenfassen, sodass Codegenerierung und anschließende Ausführung zusammenhängend stattfinden. Dies hätte für den Benutzer weniger Interaktionen zufolge, zumal die Codegenerierung an sich etwas ist, mit wem er im Prinzip nichts zu tun hat.

Mittwoch, 9. Juni 2010

Überblick über das Fachsprachsystem

In der letzten Woche habe ich grob die Anforderungen an ein System zur Formulierung von Akzeptanztests vorgestellt. Nun möchte ich einen allgemeinen Überblick über das System geben, wobei es sich hier nicht um eine genau Architekturbeschreibung handelt, sondern eher um eine Ablaufbeschreibung aus der Sicht des Benutzers.

Die Ideen dazu entstanden aus den verschiedenen Anforderungen, aber auch aus meinen bisher erlangten Kenntnissen über den Aufbau einer Anwendung auf Xtext- und EMF-Basis, womit das System ja auch schließlich umgesetzt werden soll.

Um den Aufbau des Systems zu erläutert, habe ich erst einmal folgendes Diagramm entwickelt, welches die wesentlichen Komponenten und deren Zusammenspiel zeigt. Dabei handelt es sich nicht um ein Standard-Diagramm, sondern um eine eigene Schöpfung, die lediglich zur groben Veranschaulichung dienen soll:

AblaufÜberblick

Projekt anlegen

Den Anfang bildet ein sogenanntes “Akzeptanztest-Projekt”, welches der Benutzer in seiner Entwicklungsumgebung (sprich Eclipse) anlegt. Theoretisch käme es hier nicht darauf an, unbedingt einen speziellen Projekttyp zu entwickeln, jedoch kann es von Vorteil sein, denn wenn der Benutzer einen Assistenten zur Verfügung hat, welche ihm ein Projekt und eventuelle vorgegebene Beispieldateien erzeugt, so fällt der Einstieg leichter. Jeweils ein Projekt sollte Akzeptanztests zu einer bestimmten Domäne beinhalten, jedoch ist dies kein Muss.

Akzeptanztest anlegen

Prinzipiell legt der Benutzer nachfolgenden für jeden zu testenden Use-Case einen Akzeptanztest an. Solch ein Akzeptanztest besteht vordergründig aus einer Textdatei, welcher der DSL, also der Fachsprache des Akzeptanztestsystems zugeordnet ist. In dieser DSL-Datei beschreibt der Benutzer den Anfangs- und Endzustandes seines Tests. Im Normalfall würde der Anfangszustand durch das Erzeugen von bestimmten Objekten, oder Daten in der Datenbank, bestimmt werden, wobei im Endzustand überprüft werden würde, wie sich diese Objekte beispielsweise verändert haben. Eine Möglichkeit wäre auch, hier Anfangs- und Endzustand jeweils in eine separate Datei zu speichern.

Ähnlich wie beim Anlegen eines Projektes profitiert der Benutzer auch hier von einer Assistenten-Funktion, da für einen Akzeptanztest nicht nur die jeweilige DSL-Datei, sondern auch andere Resourcen nötig sind, welche ihm so automatisch erzeugt werden können.

Importieren eines Domänen-Modells

Damit der Benutzer überhaupt in der Fachsprache Objekte erzeugen kann, benötigt er erst einmal ein Modell, welches die in seiner Domäne vorhandenen Begriffe beinhaltet. Dieses Modell ist das dem Eclipse Modeling Framework (EMF) zugrunde liegende Ecore-Modell. Die im Modell vorhandenen Begriffe (in Ecore Klassen u.ä.) werden prinzipiell auf eine Datenbank übertragen. Dabei ist höchstwahrscheinlich eine Transformationsschicht notwendig, da sich die im Modell vorhandenen (Fach)-Klassen-Welt von den Entitäten in der Datenbank unterscheiden kann. Der Benutzer agiert aber letztendlich nur mit dem Modell.

Des weiteren ist es wichtig, dass ein Modell aus einer bereits vorhandenen Datenbank erzeugt werden kann. Bei sehr großen Datenbanken wäre es sinnvoll, wenn nur bestimmte Tabellen aus der jeweiligen Datenbank in das Modell übernommen werden würde. So könnten für verschiedenen Akzeptanztests verschiedenen Modelle verwendet werden, welche jeweils nur die benötigte Untermenge der Domäne enthalten.

Definition von eigenen Begriffen

Der Benutzer soll die Möglichkeit haben, seine Domäne auf Basis des vorhandenen Modells zu erweitern. Dazu soll es ihm möglich sein, neue Begriffe zu definieren, sofern er dies als nötig betrachtet, beispielsweise, wenn er dies häufig wiederverwendet. Dazu erzeugt er in neues Objekt aus einer Klasse im Modell, welches bereits bestimmte vorbelegte Werte hat, oder auch eine Kombination aus mehreren dieser Objekte. Er erzeugt sozusagen eine neue, spezialisierte Klasse, welcher aber nicht im Domänen-Modell existiert, sondern nur in der DSL-Sprache. Dazu stellt dem Benutzer die DSL wiederum Sprachelemente zur Verfügung. Ihm ist dann freigestellt, ob er diese Definitionen separat definiert und sich damit eine Art Bibliothek anlegt, oder ob er sie direkt in seinem Akzeptanztest beschreibt, egal ob in der selben Datei, wie der Akzeptanztest an sich, oder in einer eigenen Datei. Die Import-Funktion der DSL macht es möglich, Inhalt auf beliebig viele Dateien zu verteilen. Dies soll durch die teilweise Überscheidung der jeweiligen Blöcke im Bild deutlich gemacht werden.

Interpretieren der DSL/Generieren von Code

Hat der Benutzer unter Verwendung des Domänen-Modells und eventueller Definitionen einen Anfangs- und Endzustand für den Akzeptanztest beschrieben, so lässt er diesen mithilfe einer Workflow-Datei (die bestenfalls ein Assistent bereits erzeugt hat) in entsprechenden Persistenz-Code umsetzen. Dabei werden mehrere Java-Klassen erzeugt, welche mittels des Teneo-Plugins aus dem Eclipse Modeling Project für Hibernate oder EclipseLink Code erzeugen, um die in den DSL-Dateien erzeugten Daten in der Datenbank unterzubringen. Teneo bedient sich hier wieder des Ecore-Modells, um die Daten in den jeweiligen Tabellen zu persistieren.

Ausführung des Tests

Mittels einer Art Skript (ich denke hier zum Beispiel an die Verwendung von Apache Ant) kann man nun die erzeugten Java-Klassen starten. Selbstverständlich muss hier zwischendurch die eigentliche Funktionalität, welche den Use-Case überhaupt erst umsetzen sollte, eingebunden werden. Dies ist jedoch nicht Aufgabe des Akzeptanztest-Systems, so wird sie als externes Artefakt in das Skript eingebunden.

Hier wird es sich zeigen, ob diese Art des Aufrufs praktikabel ist, oder ob sich hier eine andere Möglichkeit bietet. Statt das der Benutzer das Skript aufrufen muss, könnte dieses auch automatisiert bei Regressionstest und ähnlichem ausgeführt werden.

Zusammenfassung und Ausblick

Ich denke, der vorgestellte Aufbau bzw. Ablauf des Systems, welches ich mir vorgenommen habe, umzusetzen (zumindest zu einem großen Teil :-) ), ist im Großen und Ganzen umsetzbar und auch brauchbar für einen Fachbereichsmitarbeiter als Benutzer. Viele Dinge sollte ihm einfach von Assistenten abgenommen werden, nicht zuletzt, um den Überblick zu bewahren, gerade wenn es viele Akzeptanztests in einem Projekt gibt. Gerade hier muss auch jeder Test eindeutig identifizierbar sein und in einer geordneten Struktur abgelegt werden. Deshalb ist es von Vorteil, dem Benutzer einige Vorgaben zu machen.

Eine große Herausforderung wird es, die Domänen-Modelle, insbesondere das Ecore-Modell an sich, aus einer bereits vorhandenen Datenbank zu erzeugen. Momentan ist mir noch kein Weg bekannt, um dieses zu erreichen. Hier wird vielleicht eine separate Erstellung des Modells mit dazugehörigem Mapping auf die Datenbank und der damit verbundenen Kooperation zwischen Fachbereichsmitarbeiter und Entwickler nicht zu vermeiden sein. Jedoch stellt dies auch eine große Möglichkeit dar, denn die Funktion, einzelne (Unter-)Modelle der Domäne zu erzeugen, sollte in dem System nicht fehlen. Einer meiner nächsten Posts wird sich sicher damit beschäftigen.

Donnerstag, 3. Juni 2010

Anforderungen an ein Fachsprachsystem zur Formulierung von Akzeptanztests

In den letzten Posts habe ich eine Einführung in Xtext und Co. gegeben, welche als Technologie dazu dienen sollen, das oben genannte System umzusetzen. Nun ist es an der Zeit, einmal kurz von der Praxis wegzugehen und festzulegen, wie das System aussehen soll, d.h. welche Funktionen es besitzen soll und wie es in seiner Verwendung aussehen soll.

Dazu sind erst einmal die funktionalen Anforderungen an das System wichtig. Der Kern des Systems ist es natürlich, Akzeptanztests zu erstellen. Da wir uns hier auf datenbankgestützte Anwendungen beziehen, muss ein Akzeptanztest stets sicherstellen, dass eine bestimmte Funktion einer zu entwickelnden Software einen bestimmten Datensatz in der Datenbank ändert oder erstellt. Um dies zu überprüfen, muss der Benutzer in der Lage sein, einen Anfangszustand in der Datenbank zu beschreiben, indem er zum Beispiel bestimmte Daten erzeugt, und außerdem einen Endzustand zu bestimmen, welcher am Ende des Tests überprüft wird.

Zum Erstellen der Akzeptanztests verwendet der Benutzer eine Fachsprache, welche die nötigen Funktionen mitbringen und prinzipiell mit jeder Datenbank funktionieren muss. Um dies umzusetzen, muss die Fachsprache ein Meta-Modell verwenden, welches Datenbank-ähnliche Strukturen bietet. Für jede Datenbank muss ein konkretes (Domänen-)Modell existieren, welches auf dem Meta-Modell basiert. So sollte es möglich sein, das Akzeptanztest-System für verschiedene Domänen zu benutzen.

Nachfolgend habe ich eine Liste mit funktionalen Anforderungen an das System ausgearbeitet. Diese habe ich in Gruppen eingeteilt, dabei wird zwischen den Funktionen unterschieden, welchem unmittelbar vom Benutzer in Anspruch genommen werden und denen, die das System an sich sonst noch anbieten muss.

Funktionen bezogen auf den Benutzer/die DSL

  • der Benutzer kann Anfangs- und Endzustand der Daten für einen Akzeptanztest in einer geeigneten DSL beschreiben
  • die DSL muss Möglichkeiten bieten, Daten zu erzeugen, auf Vorhandensein zu überprüfen und ggf. zu löschen
  • der Benutzer muss ein Modell auf Basis der Datenbank erzeugen bzw. selbst erstellen können, welches als Grundlage für die Beschreibung der Testdaten dient
  • der Benutzer muss in der Lage sein, basierend auf Entitäten aus dem jeweiligen Modell eigene Fachliche Begriffe zu definieren, welche er in der DSL verwenden kann
  • erstellte Akzeptanztests sollten aufeinander aufbauen können, d.h. eine DSL-Datei kann eine andere importieren und deren Definitionen weiterverwenden
  • der Benutzer sollte in der Lage sein, die Akzeptanztests aus dem Programm aufzurufen

Automatische Funktionen des Systems

  • das System erstellt automatisch auf dem Modell basierende Persistenz-Klassen aus dem formulierten Akzeptanztest hinaus
  • formulierte Akzeptanztests müssen in Artefakte verwandelt und in einer automatisierten Umgebung nutzbar gemacht werden
  • möglichst viele Funktionen sollten dem Benutzer durch Assistenten u.Ä. abgenommen werden

Erweiterte Funktionen

  • das Importieren der Datenbankstruktur in eine Modell muss in Bezug auf sehr große Datenbanken auf bestimmte Schemas/einzelne Tabellen beschränkbar sein
  • das System muss in der Lage sein, die technische Sicht der Datenbank von der fachlichen Sicht zu trennen (ggf. über eine Transformation im Modell)

Daraus ergeben sich auch einige nichtfunktionale Anforderungen, auf welche ich jetzt aber nicht gesondert eingehen möchte. Zu erwähnen wäre hierbei jedoch beispielsweise die leichte Benutzbarkeit des Systems, welche mit der Benutzerzielgruppe des Fachbereichsmitarbeiters bzw. Analysten einhergeht.

Basierend auf diesen Anforderungen werde ich beginnen, eine Architektur bzw. zunächst einmal einen Ablaufplan für die Verwendung und Funktionalität des Systems zu erarbeiten. Dies wird in den nächsten Beiträgen Gegenstand der Betrachtung sein.

Samstag, 29. Mai 2010

Workflow eines Xtext-Projektes

Beim letzten Mal haben wir uns mit der Erstellung einer Sprache in Xtext beschäftigt, genauer gesagt mit der Verwendung der Xtext Grammatik zum Erstellen eines eigenen Modells, im Grunde genommen also mit dem Kern der DSL.

In diesem Beitrag würde ich gern den Workflow beschreiben, welchen das Xtext-Projekt durchläuft. Ich hoffe, damit erst einmal ein grundsätzliches Verständnis für meine Pläne zur Umsetzung eines Akzeptanztest-Systems, welche ich in den kommenden Posts vorstellen möchte, aufbauen zu können. In dem System werden sich einige Elemente wiederfinden, denen man auch in Xtext begegnet.

Erzeugung der Artefakte mit der Modeling Workflow Engine (MWE)

Sobald man ein Xtext-Projekt angelegt hat, so findet sich neben der Grammar-Datei mit der Dateiendung xtext eine Workflowdatei mit dem Namen Generate[Sprachname].mwe:

XtextWorkflowDSLProject

Im Bild sieht man den Package-Explorer mit der Ansicht des auch schon im letzten Post verwendeten Projekts. Neben der Workflow-Datei gibt es noch eine Properties-Datei mit einigen projektspezifischen Eigenschaften, welche der Workflow verwendet.

Der Inhalt solch eines Workflows soll uns an dieser Stelle nicht interessieren, dies würde den Umfang des Posts sprengen. Was man wissen muss, ist, dass ein MWE-Workflow einen sequenziellen Aufruf von verschiedenen Komponenten zur modellgetriebenen Entwicklung ermöglicht, daher "Workflow". Das Ganze geschieht in einer XML-basierten deklarativen Notation, außerdem ist es möglich, eigene Komponenten zu schreiben. Mehr dazu findet man auf der MWE-Seite im Eclipsepedia. Ich werde auf diese Technologie an einem anderen Zeitpunkt genauer eingehen.

Führt man den Workflow aus (Rechtsklick –> Run as –> MWE Workflow), so erzeugt Xtext alle benötigten Artefakte, vom Ecore-Modell über das EMF-GenModel (welches wiederum den Java-Code erzeugt) bis hin zu den UI-spezifischen Komponenten wie Editor und Contentassistance.

Diesen Vorgang muss man jedes mal wiederholen, sobald man etwas an der Grammatik geändert hat.

Generiertes UI-Plugin/Projekt

Das vom Xtext-Projekt Assistenten erzeugte Projekt mit der Endung ui wird erst nach dem ersten Durchlauf des Workflows mit Inhalten gefüllt:

XtextWorkflowUIProject

Hier hat man die Möglichkeit, den Proposalprovider anzupassen, einen Labelprovider zu erstellen etc., also sein Plugin für die Sprache anzupassen. In meinem Projekt für die Erstellung eines Akzeptanztest-Systems wird hier ein Wizard hinzukommen, der die Erstellung des Projektes oder auch von Akzeptanztests unterstützt.

Über die plugin.xml-Datei kann man das Plugin starten und den automatisch erstellten Editor benutzen. Man erstellt dann lediglich ein neues, leeres Projekt und eine Datei mit der vorher festgelegten Endung:

XtextWorkflowNewProject

Hier zeigt sich schon eine hervorragende Möglichkeit zur Erweiterung des Plugins: das Akzeptanztest-System könnte durch einen Assistenten die Möglichkeit zum automatischen Erstellen einer DSL-Datei mit der richtigen Endung zur Verfügung stellen.

Generiertes Generator-Projekt (optional)

Zusätzlich zu dem DSL-Projekt und dem UI-Projekt kann man sich ein Generator-Projekt (endet mit .generator) angelegen lassen. Im Xtext-Projekt-Wizard muss man dazu an der entsprechenden Stelle ein Häkchen setzen.

Das erzeugte, eigenständige Projekt dient dazu, aus dem durch Xtext erzeugten Modell Code zu generieren. Damit ist nicht nur Java-Code gemeint, sondern prinzipiell irgendein Code, dies könnten zum Beispiel ein XML-Dokument sein oder SQL-Statements, je nachdem, welchem Zweck das zugrundeliegende Modell dient.

Um dieses Ziel umzusetzen, bedient man sich hier der Codegenerierungssprache Xpand. Der Projekt-Wizard erzeugt im Generator-Projekt bereits einige Standard-Dateien, wie eine DSL-Datei der Xtext-Beispiel-DSL "Entities" und ein Xpand-Template, welches auf diesem Modell arbeitet:

XtextWorkflowGenProject

Des weiteren gibt es hier ein Xtend-Datei, welche von dem Xpand-Template eingebunden wird. Mit Xtend kann man Erweiterungen zu seinem Model schreiben, ohne das Modell zu ändern, ebenso wie eigens definierte Operationen, welche das Modell benutzen. Genauer möchte ich darauf nicht eingehen, da es für meine Zwecke nicht so wichtig ist. Dennoch sollte es nicht unerwähnt bleiben.

Die Beispieldateien würde man löschen und gegen das eigene Modell ersetzen. Was man nun mit Xpand machen kann, werde ich in einem neuen Post zeigen.

Aufgerufen wird Xpand wiederum durch eine Workflow-Datei, welche sich im Projekt befindet. Insgesamt hat dies jedoch wenig Sinn, wenn es nicht aus Plugin aus aufgerufen wird. Wie würde man also dazu vorgehen?

Man startet das Plugin über das UI-Projekt und legt in der neuen Workbench ein Projekt an. Dieses Projekt muss als Dependency das Generator-Plugin besitzen, um auf den Generator und die Xpand-Templates zugreifen zu können. Der im Generator vorhandene Workflow kann nun aus einem eigens erstellten Workflow aufgerufen werden. Dieses Beispiel stammt von der Seite von Peter Friese:

<workflow>
<cartridge file="workflow/EntityGenerator.mwe" model="classpath:/model/MyModel.entity"/>
</workflow>

Der Generator wird über den Workflow aufgerufen und verwendet dann die in der aktuellen Workbench erzeugte DSL-Datei, anstatt die, welchem im Generator-Projekt erzeugt wurde.

In dem oben genannten Artikel von Peter Friese findet man noch mehr Informationen zur Code-Generierung mit Xpand.

Src gegen Src-gen

Jedes der angelegten Projekte verfügt über zwei verschiedenen Source-Ordner. Einmal src, wo sich die Dateien befinden, die der Entwickler selbst ändert. Dann gibt es den Ordner src-gen, in welchem sich fast ausschließlich von Xtext erstellter Code befindet, wie zum Beispiel die Java-Klassen zum erzeugten Modell. Diese Dateien sollte man möglichst nicht verändern, da sie vom Xtext-Generator eh wieder überschrieben werden. Ebenso sollte man eigene Klassen und sonstige Dateien wie eigene Workflows stets in den src-Ordner legen.

Validation

Auch wenn man seine Xtext-Grammatik optimal angelegt hat, so kann man nicht immer vorhindern, dass die DSL eventuell an bestimmten Stellen Inhalte akzeptiert, welche man da eigentlich nicht haben wollte. Auch durch Referenzen kann dieses Problem oft nicht gelöst werden.

Die Lösung bietet ein von Xtext angebotenes Validator-Fragment, welches dem Workflow zur Generierung der Artefakte hinzugefügt wird. Letztendlich beschreibt man in einer Java-Klasse Methoden, welche Überprüfungen von einzelnen Regeln der Grammatik anstellen. Was man genau in diesen Methoden überprüft, ist jedem selbst überlassen.

Genaueres dazu wird es in einem kommenden Post von mir geben, da ich schon mehrfach Gebrauch von dieser Funktion gemacht habe. Inzwischen finden sich Informationen darüber i, Xtext User Guide in Chapter 5 – Unterpunkt "Validation".

Fortsetzung folgt…

In meinem nächsten Post habe ich vor, bereits einmal die prinzipiellen Ablauf, welchen ich mir für das Akzeptanztest-System vorgestellt habe, zu erläutern. Weitere Posts zu Validation und anderen Details möchte ich dann bereits an diesem System demonstrieren.

Mittwoch, 26. Mai 2010

Verwendung von XText

Mit diesem Post (und den nächsten) möchte ich ein Gefühl für die Verwendung von XText vermitteln. Dabei werde ich erst einmal auf die Erstellung der Grammatik eingehen, später auf den Workflow zur Erzeugung der Artefakte und des Plugins. Dies wird eine Basis bieten, um später die Konzepte für den Aufbau des Akzeptanztestsystems zu erläutern.

Dennoch wird es hier keine vollständige Anleitung für XText geben. Die Details kann man in der User Doku zu XText nachlesen. Hier findet man alle Möglichkeiten der Grammatik von XText und Informationen zu weiteren Konzepten, welche auch hier kurz angeschnitten werden sollen.

Außerdem empfehle ich für Beispiele, aber auch spezielle Thematiken rund um XText die Blogs von Sven Efftinge und Peter Friese. Da erfährt man, wie man zum Beispiel vorhandene Ecore-Modelle in XText importiert oder wie man mithilfe von XPand aus einer DSL Code generiert, was auch mir mittlerweile sehr geholfen hat.

Erstellung der Grammatik

Zunächst einmal habe ich eine einfache Grammatik für eine DSL zur Beschreibung von Hotels erstellt. Es ist zwar sehr einfach gehalten, sollte aber deshalb auch leicht nachzuvollziehen sein. Nachdem man ein neues XText Projekt angelegt hat (siehe XText User Guide), kann man die automatisch erstellte Grammatik folgendermaßen abändern:

grammar org.xtext.example.Testdsl with org.eclipse.xtext.common.Terminals

generate testdsl "http://www.xtext.org/example/Testdsl"

Model :
(hotels+=Hotel)*
(zimmer+=Zimmer)*
(gaeste+=Gast)*;

Hotel:
'Hotel' name=ID
'Zimmer' '{' (zimmer+=[Zimmer])+ '}'
'Gaeste' '{' (gaeste+=[Gast])+ '}';

Zimmer:
Einzelzimmer | Suite;

Einzelzimmer:
'Einzel' name=ID;

Suite:
'Suite' name=ID;

Gast:
'Gast' name=ID
'Vorname' vorname=STRING
'Nachname' nachname=STRING;

Die Grammatik beginnt mit ihrem Namen und dem Einbinden der von XText bereitgestellten Terminals. Diese Terminals, wie im Beispiel zu sehen ID, STRING und weiterhin INT sowie andere sind in einer eigenen Grammatik definiert, welche man meistens in seiner Grammatik mit verwenden möchte. Nach dem selben Prinzip lassen sich auch beliebige andere Grammatiken einbinden.

Das Schlüsselwort generate stellt den Ausgangspunkt eines wichtigen Prinzips von XText dar. Hier wird der Name des aus der Grammatik zu erstellenden Ecore-Modells angegeben. In Ecore wird das durch die DSL beschriebene Modell festgehalten, woraufhin man dieses mit den Möglichkeiten des Eclipse Modeling Frameworks weiter bearbeiten kann. Es ist auch möglich, auf die Generierung zu verzichten und stattdessen ein Modell zu importieren, bzw. eine Kombination davon. Darauf wollen wir an dieser Stelle erst einmal verzichten.

Mit dem Hintergedanken an das Modell, welches schließlich erzeugt wird, kann man nun mit Hilfe der Grammatik verschiedenen Regeln beschreiben, welche das Modell beschreiben, aber auch das Aussehen der DSL. Die EBNF-ähnliche Form der Grammatik (EBNF – Erweiterte Backus-Naur-Form) kann man relativ einfach nachvollziehen.

Die Regel Hotel zeigt grundlegende Funktionen der Grammatik:

Hotel:
'Hotel' name=ID
'Zimmer' '{' (zimmer+=[Zimmer])+ '}'
'Gaeste' '{' (gaeste+=[Gast])+ '}';

Mit Anführungszeichen eingeschlossenen Wörter gelten als Schlüsselwörter in der DSL, welche fest verankert sind und immer an ihrem vorbestimmten Platz vorkommen müssen. Sind sind lediglich dazu da, der DSL eine Struktur zu geben und sie leichter lesbar zu machen. In das Modell werden sie nicht übernommen.

Eine Regel in der DSL beschreibt meist eine Klasse im Modell. Jede Klasse bekommt bestimmte Eigenschaften oder auch Attribute, welche jeweils einen bestimmten Wert haben. In XText spricht man hier auch von Referenzen, welche die einzelnen Regeln im Syntaxbaum miteinander verbinden. Hotel besitzt ein Attribut (um in der Objektwelt zu bleiben) mit dem Namen name, welchem ein bestimmter Wert zugewiesen wird. Dieser wird hier durch die Terminal-Regel ID gebildet. Beim Parsen wird später der Inhalt dieser Regel hinter dem Schlüsselwort Hotel eingefügt, wobei es sich bei ID um eine Kombination aus Ziffern und Buchstaben handeln kann. Ein anderes Beispiel sind vorname und nachname in der Regel Gast.

Möchte man eine Liste erstellen, das heißt, kann das Attribut mehr als einen Wert annehmen, so wird dabei das = durch += ersetzt. Diesen “add”-Operator kennt man aus einigen Programmiersprachen. Ein Beispiel sieht man in der obersten Regel Model:

Model :
(hotels+=Hotel)*

Hier wird zum Beispiel beschrieben, dass die Regel Hotel mehrmals hintereinander existieren kann.

Kardinalität von Elementen

XText bietet mehrere Optionen an, um die Kardinalität, sprich die Anzahl der Elemente anzugeben:

  • keine Angabe – genau ein mal
  • ? – Maximal einmal, oder nicht vorhanden (kennzeichnet Optionalität)
  • + – bei mehreren Elementen, 1 mal oder mehr
  • * – bei mehreren Elementen, 0 oder mehr

Möchte man beispielsweise name aus dem oberen Beispiel optional werden lassen, so würde man es so beschreiben:

'Hotel' (name=ID)?

Beispiele für Listen mit mindestens einem oder mindestens 0 Elementen findet man in der Beispiel-DSL mehrfach. So muss ein Hotel mindestens ein Zimmer besitzen, jedoch kann das Modell auch gar keine Hotels oder Gäste besitzen.

Cross References

Bis jetzt haben wir uns nur mit dem Zuweisen von Regeln beschäftigt. An der entsprechenden Stelle wird die Regel vom Parser eingesetzt. Man kann jedoch auch Objekte durch Regeln an einem Ort beschreiben und diese dann wieder an einem anderen Ort wiederverwenden. Ein Beispiel:

'Hotel' name=ID
'Zimmer' '{' (zimmer+=Zimmer)+ '}'

Würde man die Regel in dieser Weise ausführen, so müsste man die Zimmer direkt in die geschweiften Klammern des Hotels schreiben, also:

Hotel hotel1
Zimmer { Einzel ... }

Geht man wieder zu der oben verwendeten Schreibweise zurück, so kann man an dieser Stelle Referenzen verwenden. Diese werden dadurch gekennzeichnet, dass man die entsprechende Regel in eckige Klammern setzt. Dies bedeutet dann so viel wie: hier wird ein Objekt/ eine Instanz vom Typ X eingesetzt.

'Hotel' name=ID
'Zimmer' '{' (zimmer+=[Zimmer])+ '}'

Nun würde man vorher ein Zimmer definieren, ihm einen Namen geben und diese Referenz dann verwenden.

Suite zimmmer1

Hotel hotel1
Zimmer { zimmer1 }

Wichtig hierbei ist: um eine Referenz wieder zu finden, muss die entsprechende Regel ein Attribut mit dem Namen name besitzen, so wie es schon mehrfach in der Beispielgrammatik vorkommt. Ich denke, dies hängt direkt mit dem Ecore-Modell zusammen. Die meisten Elemente des Ecore Meta-Modells erben von einer Klasse namens ENamedElement, welche für alle Elemente ein Attribut name definiert und sie so identifizierbar macht. Alle anderen Attribute, die nicht zur Referenzierung verwendet werden, kann man natürlich beliebig benennen.

Verzweigungen / Alternativen

Oben in der Grammatik gibt es eine Verzweigung bei Zimmer, wobei die Regel Zimmer zwei Alternativen bietet: Einzelzimmer oder Suite.

Zimmer:
Einzelzimmer | Suite;

Ob diese Konstellation so sinnvoll ist oder nicht sei dahingestellt, jedoch lässt sich hier gut der Sinn solch einer Verzweigung darstellen. Sieht man das Ganze wieder im Hinblick auf das später erzeugte Modell, so erzeugt man hier eine Art der Vererbung. Zimmer ist die Superklasse und Einzelzimmer bzw. Suite sind die spezialisierten Klassen. In unserem Beispiel macht dies nicht so ganz Sinn, denn beide fügen keinerlei Spezialität hinzu sondern besitzen nur das gleiche Attribut name.

Hierbei sieht man aber eine Besonderheit im Aufbau solch einer Hierarchie. Alle gleichen Attribute, das heißt, eigentliche solche, die in die Superklasse kommen würden, müssen hier jeweils in den Regeln aller Kindklassen vorkommen. Die Regel Zimmer ist hier nur dazu da, die Verzweigung zu bestimmen. Im später erzeugten Modell werden dann alle gleichen Attribute weiter oben angesiedelt, wie man im folgenden Abschnitt sehen wird.

Eine weitere Einsatzmöglichkeit solcher Verzweigungen ist, den Ablauf der Sprache etwas beliebiger zu gestalten, sodass man beispielsweise Hotels, Zimmer und Gäste nicht in der genauen Reihenfolge beschreiben muss, sondern diese durch eine Verzweigung einer gemeinsamen Superklasse zuordnet, wodurch es möglich wird, die Reihenfolge völlig beliebig zu gestalten.

Das Modell hinter der Grammatik

Ohne momentan den Workflow zur Erstellung des Modells zu betrachten (das kommt beim nächsten Mal), möchte ich gern einmal so ein Modell vorstellen, und zwar genau das, welches durch die oben beschriebene Grammatik erstellt werden würde.

Ecore-Modell für das Beispiel

In dem Ecore-Modell wird für jede Liste eine EReference (gekennzeichnet durch den Pfeil am Symbol) angelegt, für jedes einzelne Attribut ein EAttribute. Wie man hier sieht, wird die Verzweigung bei Zimmer dadurch aufgelöst, dass hier Unterklassen gebildet werden. Alle gemeinsamen Attribute kommen dabei in die Superklasse, was in unserem Fall nur name ist.

Dies ist noch ein sehr einfaches Beispiel. Wir werden sehen, dass im Laufe der Entwicklung des AKzeptanztest-Systems ein sehr viel komplizierteres Modell herauskommen wird, vor allem da ein Modell nötig sein wird, welches nicht nur eine Domäne wie hier “Hotel”, sondern beliebig viele unterstützen muss.

Ein konkretes Beispiel zum Abschluss

Zum Schluss folgt ein konkreter Text in der oben vorgestellen DSL:

Hotel hotel1
Zimmer { zimmer1 zimmer2 }
Gaeste { gastMuster }

Einzel zimmer1
Suite zimmer2

Gast gastMuster
Vorname "Max" Nachname "Mustermann"

Hier erhält man nun einen Eindruck, was aus der oben beschriebenen DSL werden kann. Man beachte die Reihenfolge insbesondere der Schlüsselwörter und die Verwendung der Referenzen. Im Laufe der Zeit werde ich in diesem Blog immer mal wieder einzelne Details von XText vorstellen, welche zur Umsetzung des Akzeptanztest-Systems nötig sind.

Im nächsten Post habe ich vor, die Erstellung eine DSL aus der Sicht des Workflows zu betrachten, welchen man durchführen muss, um aus der Grammatik ein Modell zu machen und ein Plugin zu erzeugen, mit welchem man dann einen konkreten Text in der DSL schreiben kann.