Tests innerhalb einer MDD-Infrastruktur anhand von JooMDD

Aus THM-Wiki
Wechseln zu: Navigation, Suche
Kurs
Kursname Modellgetriebene Softwareentwicklung in der Praxis SS 16
Kurstyp Praktikum
Dozent Priefer
Semester SS 16
Studiengang Master
Link http://homepages.thm.de/~dprf53


Überblick

JooMDD ist eine Infrastruktur zur modelgetriebenen Entwicklung von Joomla-Erweiterungen. Basierend auf der Domänen spezifischen Sprache eJSL, die konkret für Joomla entwickelt wurde, können Joomla-Erweiterungen bis zu einem gewissen Grad aus dem Model generiert werden.

Zur Unterstützung der Entwicklung der DSL und des Generators, soll nun im Rahmen dieses Projektes eine Testumgebung realisiert werden. Dabei sind unter Verwendung von xtend, jUnit Tests zu erstellen um die unterschiedlichen Sprachkonstrukte und die Generatorausgaben zu testen. Des weiteren sollen Validationsregeln und Quick-Fixes für die DSL erstellt werden um die Entwickler bei der Erstellung von Modellen auf Basis von eJSL zu unterstützen.

Als Ausgangspunkt der Testentwicklung soll zunächst ein eJSL-Referenzmodell erstellt werden.

Aufgabenstellungen

  • Erweiterung und Aktualisierung der Validationsregeln/Quick-Fixes
    Die bestehenden Validationsregeln der DSL sollen überarbeitet werden und um zusätzliche Regeln erweitert werden.
    Beispiele für geforderte zusätzliche Regeln sind:
    • Für Indexpages mit mehreren Entities muss geprüft werden, dass die Entities jeweils eine Referenz auf die erste (Main-)Entität besitzen.
    • Unterbindung der Verwendung von Unterstrichen bei der Bennenung von Pages, Entities und Extensions

  • Entwicklung von Parsingtests
    Hierzu sollen Beispieltemplates erstellt werden und anhand dieser Tests für die Grundfunktionalitäten (Simple-Tests) wie Pages, Entities und Extensions
    entwickelt werden. Zusätzlich sollen weitere Tests für komplexere Funktionalitäten (Komplexe-Tests) wie Links und Referenzen erstellt werden.

  • Entwicklung von Generatortests (EJSL Generatortest)
    • Hinzufügen weiterer Generatortests zur Prüfung der Installierbarkeit der generierten Erweiterung (Installations-Script-Test).
    • Test der generierten Joomla Dateistruktur auf validität (Simple/Komplex)
    • Ressourcentransformator Mapping- und Transformationstests

  • Portierung der Quick-Fixes für die InteliJ-Umgebung
    Die in Eclipse erstellten Quick-Fixes sollen auch in der InteliJ-Umgebung verwendet werden können.

Entwicklung von Parsingtests

Referenzmodelle für Tests

Als Grundlage für die zu erstellenden Parsingtests sollten zunächst Referenzmodelle für einfache
sowie komplexe Strukturen, mit Berücksichtigung von Links und Referenzen, entwickelt werden.
Für die Überprüfung von komplexen Strukturen wurde dazu ein Model für ein Shop-System erstellt.

Entitys des Shopmodells

Die in jooMDD verwendete DSL 'eJSL', benötigt für die generierung aller Hauptelemente des Modells
wie 'Produkt' oder 'Kunde', deren Definition als sog. 'Entity'.
Für das beispielhafte Shopmodell wurden folgende Entitys definiert:


Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Entity - creditInstitute, location, ^Order
Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Entity - product, supplier, customer


Pages des Shopmodells

Um aus den definierten Entitäten die Joomla typische, MVC-Verzeichnis und Dateistruktur generieren zu
können, musste für jede Entität eine Page definiert werden. Dabei wurde für jede Entität eine Index und eine Details-Page erstellt.

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Übersicht aller definierten Pages


Extension-Teil des Shopmodells

Über 'extensions' werden in eJSL grundlegende Parameter wie Author, Lizenz, unterstützte Sprachen für die
zu generierende Joomla-Erweiterung festgelegt.Darüber hinaus werden über 'sections' die zuvor definierten
Pages den zwei Hauptbereichen, Backend und Frontend, von Joomla zugeordnet.
Für das Shop-Referenzmodell wurden folgende Definitionen erstellt:

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Extension
Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Aus Model generierte Joomla-Erweiterung

Damit ist das Shop-Referenzmodell abgeschlossen und konnte im nächsten Schritt als Grundlage
für die Erstellung von JUnit-Tests der Sprache eJSL verwendet werden.

eJSL-Testentwicklung auf Basis des Shop-Referenzmodells

Nach der Erstellung des Referenzmodells konnte nun mit der Entwicklung der jUnit-Tests begonnen werden.

Die zu erstellenden Testfälle sind in dem Projektordner de.thm.icampus.joomdd.ejsl.tests unter dem Projektverzeichnis src/de.thm.icampus.joomdd.ejsl.tests/ in der Datei EJSLParsingTest.xtend zu finden.

Die in dieser Datei definierten xtend/jUnit Tests, werden automatisch im xtend-gen Verzeichnis des Projekts in Java generiert (EJSLParsingTest.java) sobald die Änderungen gespeichert werden.

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Testdateien

Um das zuvor erstellte Referenzmodell in den Tests zu nutzen muss dieses zunächst als String aus der Shop.eJSL Datei importiert werden. Anschließend kann eine Instanz des Modells über den von xtend bereitgestellten ParserHelper generiert werden.

 .
 .
 .

    //import reference model instance (All tests are based on this model)
    public String reference = new Scanner(new File(...yourPath.../de.thm.icampus.joomdd.ejsl.ui/templates/Shop.eJSL')).useDelimiter('\\A').next();
	
	@Inject
	ParseHelper<EJSLModel> parseHelper;
	
	//test the eJSL Entity with attributes and their types
	@Test
	def void createEntityAttributes() {
		
		//parse the reference model
		val instance = parseHelper.parse(reference)
 
 .
 .
 .

Nachfolgend wurden nun mehrere Testfälle für die simplen und komplexen Sprachkonstrukte des eJSL-Modells erstellt. Dabei wurde mittels Assertions abgefragt, ob die generierte Instanz aus dem Referenzmodel den erwarteten Werten entspricht.

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Übersicht der bisherigen Testfälle

Getestet wurde jeweils in zwei Schritten, erst wurde auf das vorhandensein der Übergeordneten Elemente geprüft (Simple Test) und anschließend die innerere Struktur auf Existenz, Korrektheit der Werte und die Referenzen geprüft (KomplexTest).


Beispiel für Simple Test:

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Tests der einfachen Strukturen

Beispiel für komplexen Test:

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Tests der komplexen Strukturen

Simples Model

Neben dem Shop-Modell für komplexe Tests wurde zusätzlich ein Simple-Modell erstellt um einfachere Strukturen der Sprache zu testen:

  • Entities
    • Die Entities des simplen Modells:
Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Entities des SimpleModell
  • Pages
    • Die Pages des simplen Modells:
Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Pages des SimpleModell
  • Extension
    • Der Extension-Teil des simplen Modells:
Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Extension des SimpleModell

Erweiterung und Aktualisierung der Validationsregeln/Quick-Fixes

Für EJsl existierten bereits Validationsregeln und Quick-Fixes in einer älteren Version von JooMDD, jedoch waren diese zeitweise aufgrund von Versionsänderungen in Xtext bzw. Xtend nicht mehr lauffähig. Daher bestand die erste Aufgabe darin, die bestehenden Validationsregeln/QuickFixes für die aktuelle Version lauffähig zu machen. Dafür wurde zunächst der alte Stand aus dem JooMDD Repository kopiert und in das aktuelle eingefügt.

Es stellte sich heraus das die Validationsregeln und QuickFixes nach einigen imports von EJsl Strukturen wieder funktionierten, sodass diese ohne weiteres weiterverwendet werden konnten.

Validierungsregeln

Zu der bereits sehr umfassenden Regelsammlung wurden unter anderem folgende in der Aufgabenstellung geforderten Regeln ergänzt:

  • checkAttributeReferenceToEntity
    • Für Entities die ein Attribut besitzen, dass gleichnamig mit einer bestehenden Entität ist, wird eine Warnung angezeigt.
      Für dieses Attribut sollte eine Referenz auf die jeweilige Entität angelegt werden.
/**
	 * Check Entity with other Entity as Attribute has a Reference to it
	 */
	@Check
	def checkAttributeReferenceToEntity(EJSLModel model){
		var entities = new HashSet<String>
		var refs = new HashSet<String>
		
		// Save all Entity names
		for (entity : model.getEjslPart.getFeature.getEntities) {
			entities.add(entity.name)
		}
		
		// Run through all Entities
		for (entity : model.getEjslPart.getFeature.getEntities) {
			
			// Run through all attributes of that Entity
			for (attribute : entity.attributes) {
				
				// Check for every attribute if there is a equal named Entity
				for (eName : entities) {
					
					if (attribute.name.toLowerCase == eName.toLowerCase) {			
						
						// Check if Reference exists
						for (references : entity.references){
							refs.add(references.entity.name.toLowerCase)
						}
						
						// When no Reference to that Entity exists, show a warning message
						if (refs.add(eName.toLowerCase)) {
							warning(
								'Attribute ' + attribute.name + ' should have a Reference to matching Entity. Make sure referenced Entity is defined above',
								attribute,
								EJSLPackage.Literals.ATTRIBUTE__NAME,
								MISSING_REFERENCE
							)
						}
					}
					
					// Reset references
					refs.clear
				}
			}
		}
	}

Funktionsweise:

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
checkAttributeReferenceToEntity
  • checkNoUnderscoreIn_____Name
    • Unterstriche in Entity, Page und Extension Namen sind nicht mehr erlaubt und erzeugen einen Error.
	/**
	 * Checks if the name of a page contains a underscore
	 */
	@Check
	def checkNoUnderscoreInPageName(EJSLModel model) {
		for (page : model.ejslPart.getFeature.pages) {
			if (page.name.contains('_')) {
				error(
					'Page name ' + page.name + ' contains a underscore',
								page,
								EJSLPackage.Literals.PAGE__NAME,
								FORBIDDEN_UNDERSCORE_PAGENAME
				)
			}
		}
	}
	
	/**
	 * Checks if the name of a entity contains a underscore
	 */
	@Check
	def checkNoUnderscoreInEntityName(EJSLModel model) {
		for (entity : model.getEjslPart.getFeature.getEntities) {
			if (entity.name.contains('_')) {
				error(
					'Entity name ' + entity.name + ' contains a underscore',
								entity,
								EJSLPackage.Literals.ENTITY__NAME,
								FORBIDDEN_UNDERSCORE_ENTITYNAME
				)
			}
		}
	}
	
	/**
	 * Checks if the name of a extension contains a underscore
	 */
	@Check
	def checkNoUnderscoreInExtensionName(Extension ext) {
		if (ext.name.contains('_')) {
			error(
					'Extension name ' + ext.name + ' contains a underscore',
								ext,
								EJSLPackage.Literals.EXTENSION__NAME,
								FORBIDDEN_UNDERSCORE_EXTENSIONNAME
				)
		}
	}

Funktionsweise:

checkNoUnderscoreInPageName
  • checkMultipleEntitiesInIndexPageReferences
    • Für Pages die mehr als eine Entität als Referenz aufweisen, wird geprüft das alle auf die erste Entitätsreferenz folgenden Entities eine Referenz auf die erste (Main-Entity) besitzen.
      Falls nicht wird ein Error angezeigt und darauf hingewiesen das diese eine entsprechende Referenz benötigen.
/**
	 * Check if entities that occur in index pages with further entities, have a reference to the first entity
	 */
	@Check
	def checkMultipleEntitiesInIndexPageReferences(EJSLModel model) {
		var mainEntities = new HashSet<Entity>
		var foundReferenceEntity = new HashSet<Entity>
		
		for (page: model.ejslPart.feature.pages) {
			
			if (page instanceof IndexPage){
				
				if (page.entities.size > 1) {
					
					// Save main entity
					mainEntities.add(page.entities.get(0))
					for (int i : 1 ..< page.entities.size) {
						
						// Check if current entity has a reference to main entity
						for (referencedE : page.entities.get(i).references) {
							
							if ((referencedE.entity as Entity).name.toLowerCase == mainEntities.get(0).name.toLowerCase) {
								foundReferenceEntity.add(referencedE.entity as Entity)
							}
						}
						if (foundReferenceEntity.empty) {
							error(
								'Entity: \'' + page.entities.get(i).name + 
								'\' of IndexPage: \'' + page.name + '\' has no reference to IndexPage main-entity: \'' + 
								mainEntities.get(0).name + '\'.',
								page,
								EJSLPackage.Literals.DYNAMIC_PAGE__ENTITIES,
								i,
								NO_REFERENCE_TO_MAIN_ENTITY
							)
						}
						foundReferenceEntity.clear
					}
				}
				mainEntities.clear
			}
		}
	}

Funktionsweise:

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
checkMultipleEntitiesInIndexPageReferences
  • checkEntityNameIsNoXtextKeyword
    • Dieser Test sollte die Namen von Entities darauf prüfen ob sie mit einem bestehenden Xtext Keyword in konflikt stehen. Diese Prüfung wird jedoch dadurch verhindert, dass die Xtext interne Prüfung vorrang vor den individuellen Validierungsregeln hat. Bisher konnte dafür noch keine Lösung gefunden werden.
	/**
	 * Check entity name does not match a Xtext keyword
	 * TODO make this work, xtext validation runs before this validation...
	 */
	@Check(FAST)
	def checkEntityNameIsNoXtextKeyword(EJSLModel model) {
		for (entity : model.ejslPart.feature.entities) {
			if (XTEXT_KEYWORDS.contains(entity.name)){
				error(
					'Entity name ' + entity.name + ' is a Xtext keyword.',
								entity,
								EJSLPackage.Literals.ENTITY__NAME,
								FORBIDDEN_UNDERSCORE_ENTITYNAME
				)
			}
		}
	}

Validationsregeln in der Übersicht:

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Übersicht

Quick-Fixes

Auch die bereits in einem älteren Versionsstand vorhandenen Quick-Fixes konnten teilweise mit leichten Anpassungen wiederverwendet werden.
Zuvor wurde angenommen das sich der QuickFixProvider in der zwischenzeit geändert hat, jedoch funktioniert dieser mit der aktuellen Version wieder.

Bisherige Übersicht der Quick-Fixes

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
Übersicht

Die bestehenden Fixes waren bereits sehr umfassend, sodass an dieser Stelle nur noch wenig hinzugefügt werden konnte.
Neben den Anpassungen an den bestehenden Fixes wurden unteranderem folgende Fixes hinzugefügt:

  • deleteUnderscoreIn_____Name
    • Dieser Quick-Fix löscht ungewollte Unterstriche aus Entity, Extension und Pagenamen.
/**
	 * Delete underscore in extension name
	 */
	@Fix(EJSLValidator::FORBIDDEN_UNDERSCORE_EXTENSIONNAME)
	def deleteUnderscoreInExtensionName(Issue issue, IssueResolutionAcceptor acceptor){
		acceptor.accept(issue, 'Delete underscore', 'Delete the underscore from the extension name.', '') [
			element, context |
			
			val ext = element as Extension
			val extName = ext.name.replace('_','')
			context.xtextDocument.replace(issue.offset, issue.length, extName)
			]
	}

	/**
	 * Delete underscore in entity name
	 */
	@Fix(EJSLValidator::FORBIDDEN_UNDERSCORE_ENTITYNAME)
	def deleteUnderscoreInEntityName(Issue issue, IssueResolutionAcceptor acceptor){
		acceptor.accept(issue, 'Delete underscore', 'Delete the underscore from the entity name.', '') [
			element, context |
			
			val ent = element as Entity
			val entName = ent.name.replace('_','')
			context.xtextDocument.replace(issue.offset, issue.length, entName)
			]
	}
	
	/**
	 * Delete underscore in page name
	 */
	@Fix(EJSLValidator::FORBIDDEN_UNDERSCORE_PAGENAME)
	def deleteUnderscoreInPageName(Issue issue, IssueResolutionAcceptor acceptor){
		acceptor.accept(issue, 'Delete underscore', 'Delete the underscore from the page name.', '') [
			element, context |
			
			val page = element as Page
			val pageName = page.name.replace('_','')
			context.xtextDocument.replace(issue.offset, issue.length, pageName)
			]
	}
  • fixNonExistingPrimaryAttribute
    • Die Funktionsweise zwischen Validation und QF wurde abgeändert, sodass erkannt wird ob überhaupt Attribute vorhanden sind. Falls nein, wird zunächst ein Error angezeigt der darauf hinweist das eine Entität mindestens ein Attribut haben muss. Wurde dieses angelegt, greift anschließend die Prüfung auf das Primär-Attribut. Die Definition als Primär-Attribut kann dann über den QF hinzugefügt werden.

Validation für Fehlende Attribute/Fehlendes Primär-Attribut

/**/**
	 * Checks if entity has attributes
	 */
	@Check
	def checkEntityHasAttributes(Entity entity) {
		if (entity.attributes.size == 0 || entity.attributes == null) {
			error(
					'Entity must have attributes with at least one attribute in it.',
					entity,
					EJSLPackage.Literals.ENTITY__NAME,
					MISSING__ATTRIBUTES
				)
		}
	}
		
	/**
	 * Checks if at least one Primary attribute exists in the attributes of an entity
	 */	
	@Check
	def checkPrimaryAttributeExist(Entity entity) {
		var hasPrimary = false;
		
		if (entity.attributes.size != 0) {
			for (attribute : entity.attributes) {
			if(attribute.isunique){
				hasPrimary = true;
			}
		}
		if(!hasPrimary){	// if no primary attribute is found
			error(
					'Attributes must have a primary attribute.',
					entity.attributes.get(0),
					EJSLPackage.Literals.ATTRIBUTE__NAME,
					MISSING_PRIMARY_ATTRIBUTE
				)
		}
		}	
	}

Quick-Fix

/**
	 * Add a missing primary attribute to the first attribute of an entity
	 */
	@Fix(EJSLValidator::MISSING_PRIMARY_ATTRIBUTE)
	def fixNonExistingPrimaryAttribute(Issue issue, IssueResolutionAcceptor acceptor) {
		acceptor.accept(issue, 'Add primary attribute (define attribute first!)', 'Adding primary attribute to the first Attribute', '')[ element, context |
			val firstAttribute = element as Attribute
			firstAttribute.isunique = true
			firstAttribute.id = true
		]
	}

Funktionsweise

Fehler beim Erstellen des Vorschaubildes: Datei fehlt
fixNonExistingPrimaryAttribute

Probleme mit geplanten Quick-Fixes

  • referenceToEntity
    • Dieser QuickFix sollte es ermöglichen einer Entität die ein Attribut enthält, dass gleichnamig mit einer weiteren Entität ist, eine Referenz darauf hinzuzufügen. Jedoch führte das einfügen einer mit 'EJSLFactory.eINSTANCE.createReference()' erstellten Reference über eine ISemanticModification zu einer Fehlermeldung:
Unhandled event loop exception
!STACK 0
org.eclipse.emf.common.util.WrappedException: java.lang.RuntimeException: Could not serialize EObject via backtracking.

Modelmodifikationen konnten daher nur über xtextDocument durchgeführt werden, dass jedoch sehr limitiert ist da mit dem Offset des jeweiligen Issues gearbeitet werden muss und lediglich delete und replace Operationen auf das Dokument verfügbar waren.

Entwicklung von Generatortests (EJSL Generatortest)

Portierung der Quick-Fixes für die InteliJ-Umgebung

...