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.