Über unsMediaKontaktImpressum
Dr. Tobias Nestler, Dr. Mirko Seifert & Dr. Christian Wende 30. Juni 2015

Tools für Acceptance Test-Driven Development (ATDD)

Automatisiertes Testen bildet einen festen Bestandteil jedes modernen Entwicklungsprozesses. Entwickler schreiben Tests mit dem Ziel, die Funktionalität einer Anwendung sicherzustellen. Diese Tests werden meist in einer Programmiersprache erstellt. Daraus ergibt sich zwangsläufig eine Lücke zwischen den eigentlichen Anforderungen (wie sie ein Kunde formulieren würde) und dem Testcode, der die Anforderungen prüft (wie ihn der Entwickler schreibt).

Acceptance Test-Driven Development (ATDD) schließt diese Lücke, indem Anforderungen weitestgehend in natürlicher Sprache formuliert werden, aber gleichzeitig ihre Ausführbarkeit sichergestellt wird. Ziel von ATDD ist es, verständliche, wartbare und gleichzeitig ausführbare Testspezifikationen zu schreiben. Nachdem der erste Teil unserer Serie zu ATDD in die Methodik und das allgemeine Vorgehen eingeführt hat, widmen wir uns nun dem konkreten Vergleich einzelner Tools aus dem Eclipse-Umfeld.

Um ATDD möglichst komfortabel einsetzen zu können, braucht man die passende Werkzeugunterstützung. Gerade im Eclipse-Umfeld ist man gewohnt, auf eine gute Unterstützung der IDE zurückgreifen zu können. Daher sollen in diesem Beitrag exemplarisch die drei Werkzeuge Cucumber [1], Jnario [2] und NatSpec [3] vorgestellt werden. Die Tools unterscheiden sich sowohl technologisch als auch konzeptionell und erlauben damit einen guten Vergleich, wie die Umsetzung des ATDD-Prinzips mit unterschiedlichen Werkzeugen aussehen kann.

Um die Werkzeuge hinsichtlich ihrer Arbeitsweise besser vergleichen zu können, bleiben wir bei unserem Beispiel mit der Flugbuchung aus dem ersten Teil. Zur Veranschaulichung nutzen wir ein kleines Domänenmodell mit einigen wenigen Klassen (Flight, AirplaneType, Passenger, BookedSeat). Darüber hinaus legen wir fest, dass diese Domänenklassen mit Hilfe der Java Persistence API (JPA) in einer relationalen Datenbank abgelegt werden sollen. Dieses Setting spiegelt eine typische Situation in Java Enterprise Projekten wider, stellt jedoch keine Grundvoraussetzung für den generellen Einsatz von ATDD-Werkzeugen dar.

Cucumber

Das erste Werkzeug, das wir hier betrachten wollen, heißt Cucumber und hat seine Wurzeln in der Ruby-Welt. Nach seiner erfolgten Portierung zu Java kann es mittlerweile aber auch ohne Ruby-Kenntnisse benutzt werden. Es wurde von Aslak Hellesøy im Jahr 2008 initiiert und ist im Kern ein Kommandozeilenwerkzeug, welches Testfälle aus normalen Textfiles einliest und diese dann ausführt. Das Prinzip von Cucumber ist dabei so einfach wie genial: Testfälle – sogenannte Features – werden als Textdateien im Projekt abgelegt. Jede Feature-Datei kann dann mehrere Szenarien (Scenarios) und Anweisungen oder Schritte (Steps) enthalten. Wie in Listing 1 anhand unseres Flugbuchungsszenarios zu sehen, werden diese Schritte in natürlicher Sprache definiert.

Listing 1:

Feature: Book flight
  
  Scenario: Book a seat for a passenger
    Given a passenger Max Mustermann
    And a flight LH-1234 executed on a Boing 737
    When a seat is booked
    Then a valid ticket is issued

Anhand unseres Beispiels in Listing 1 erkennt man, dass der Aufbau eines jeden Szenarios einem Muster bzw. einer Grundstruktur folgt. Diese wird als "Gherkin" Syntax bezeichnet. Hierbei spielen verschiedene Schlüsselworte eine besondere Rolle. So beginnt jedes Feature-File mit dem Schlüsselwort Feature und jedes Szenario mit Scenario. Die einzelnen Schritte können dann jeweils mit Given, When, Then, And oder But eingeleitet werden. Darüber hinaus existieren noch weitere Schlüsselworte, z. B. Background, auf die wir an dieser Stelle aber nicht im Detail eingehen werden.

Um ein solches Szenario nun ausführen zu können, müssen wir definieren, welche Bedeutung jeder einzelne Schritt hat. Hier kommen die sogenannten "Step definitions" ins Spiel. Diese ordnen dem natürlichsprachlichen Text den passenden Code zu. Dieser Code wird als Support-Code bezeichnet, weil er die Ausführung der Szenarien unterstützt, bzw. erst ermöglicht. Somit verarbeitet Cucumber sukzessive die Beschreibungen der Szenarien und versucht den passenden Support-Code zu finden. Ist dieser vorhanden, so wird er ausgeführt. Kann keine passende Support-Methode gefunden werden, so gibt Cucumber eine entsprechende Warnung aus.

Die erwähnten Step-Definitions sind spezielle Methoden, welche mit Annotationen (z. B. @Given, @When, @Then) versehen werden können. Als Argument für die Annotationen dienen reguläre Ausdrücke, auf die der Text in den Szenariobeschreibungen matchen muss. In Listing 2 sind für unser Beispielszenario diese Methoden in der Klasse BookFlightStepDef zusammengefasst. Es lässt sich leicht erkennen, wie der Zusammenhang zwischen den Testschritten (s. Listing 1) und den aufzurufenden Methoden hergestellt wird.

Listing 2:


public class BookFlightStepdefs {
    
  private AirlineDAO dao = new AirlineDAO(getClass());
  private Passenger passenger;
  private Flight flight;
  private boolean success;
  
  @Given("^a passenger (.+)$")
  public void createPassenger(String name) throws Throwable {
    String[] parts = name.split(" ");
    String firstname = parts[0];
    String lastname = parts[1];
    passenger = dao.createPassenger(firstname, lastname);
  }
  
  @Given("^a flight (.+) executed on a (.+)$")
  public void createFlight(
      final String flightNumber, 
      final String airplaneName) throws Throwable {
    dao.executeInTransaction(new ICommand() {
      @Override
      public void execute(IDBOperations operations) {
        flight = operations.createFlight(flightNumber);
        int seats = 0;
        if ("Boing 737".equals(airplaneName)) seats = 137;
        AirplaneType airplane = 
          operations.createAirplaneType(airplaneName, seats);
        flight.setAirplane(airplane);
      }
    });
  }
  
  @When("^a seat is booked$")
  public void bookSeat() throws Throwable {
    success = dao.bookSeat(passenger, flight);
  }
  
  @Then("^a valid ticket is issued$")
  public void isTicketValue() throws Throwable {
    assertTrue(success);
  }
}

Cucumber-Tests lassen sich mit einem speziellen TestRunner für JUnit ausführen. Hierfür legt man eine leere Testklasse an und versieht diese mit der Annotation @RunWith(Cucumber.class). Natürlich lassen sich Cucumber-Tests auch im Buildprozess mit Ant oder Maven anstoßen. Es ist in diesem Zusammenhang jedoch wichtig zu erwähnen, dass Cucumber interpretativ arbeitet. Es wird kein Code für die Feature-Dateien generiert. Es gilt also darauf zu achten, dass beim Ausführen der Tests auf einem CI-System alle Features und alle Step-Definitionen im Klassenpfad liegen.

Bei der Ausgestaltung der Step-Definitionen und der Features gilt es, das richtige Gefühl für die nötige Granularität zu finden. Es ist einem weitgehend freigestellt, ob man beispielsweise sehr spezielle, große Step-Definition-Methoden schreibt oder die Szenarien aus vielen kleinen parametrisierbaren Schritten zusammensetzt. Letztendlich ist dies auch eine Frage, an welcher Stelle man den Inhalt der Tests maßgeblich gestalten will, d. h. ob man lange Feature-Dateien kurzen Step-Definitionen vorzieht oder umgekehrt.

Neben der beschriebenen Basisfunktionalität bietet Cucumber noch zahlreiche weitere Möglichkeiten. Es können beispielsweise Szenarien in verschiedenen Sprachen definiert, Features mit Hilfe von Tags strukturiert oder Schritte über Szenarien hinweg wiederverwendet werden.

Die Java-Implementierung von Cucumber ist bzgl. der Testausführung und der Integration in existierende Build-Systeme bereits recht ausgereift. Als Eclipse-Nutzer wünscht man sich natürlich vor allem einen komfortablen Editor für Feature-Files. Um diese Lücke zu schließen existieren bereits einige Projekte auf GitHub, welche an entsprechenden Plug-ins [4; 5] arbeiten.

Jnario

Das zweite Werkzeug unseres Vergleichs ist Jnario. Es wurde von Sebastian Benz und Birgit Engelmann bei der BMW Car IT GmbH entwickelt. Das Vorgehen bei der Nutzung von Jnario erinnert auf den ersten Blick an Cucumber, beim genaueren Hinsehen zeigt sich aber, dass es doch einige wesentliche Unterschiede zwischen den beiden Werkzeugen gibt.

Die Spezifikation von Features erfolgt bei Jnario in der gewohnten Cucumber Syntax, d. h. mit den Schlüsselworten wie Feature oder Scenario. Darüber hinaus bietet Jnario aber weitere Möglichkeiten an: So können beispielsweise sogenannte "Specs" benutzt werden, um das erwartete Verhalten von Anwendungen zu beschreiben. Im Gegensatz zu Features sind Jnario-Specs meist direkt mit der Klasse assoziiert, deren Verhalten getestet werden soll. Sie nutzen zudem eine andere Syntax, d.h. auch andere Schlüsselworte.

Bei Einbindung des Support-Codes geht Jnario ebenfalls einen etwas anderen Weg als Cucumber. Statt Methoden mit speziellen Annotationen zu versehen, kann der Test-Code direkt in die Feature- oder auch Spec-Dateien eingefügt werden. Hier bedient sich Jnario der Xtend-Syntax. Damit können sowohl normaler Java-Code als auch die erweiterten Sprachkonzepte von Xtend genutzt werden, um den Support-Code zu schreiben. Ein Beispiel für unser Flugbuchungsszenario mit Jnario ist in Listing 3 aufgeführt. Hier wird allerdings der Cucumber-Style und nicht die Jnario-Spec-Syntax verwendet. Man kann im Listing gut erkennen wie der Support-Code direkt in die Beschreibung des Szenarios eingebettet wird. Der Support-Code kann im Jnario-Eclipse-Editor auch ausgeblendet werden.

Listing 3:


package de.devboost.atdd.jnario
  
import org.hedl.examples.airline.custom.*
import org.hedl.examples.airline.entities.*
  
import static extension org.jnario.lib.Should.*
  
Feature: BookFlight
  
  Scenario: Book a seat for a passenger
    private AirlineDAO dao = new AirlineDAO(getClass());
    private Passenger passenger;
    private Flight flight;
    private boolean success;
    
    Given a passenger Max Mustermann
      passenger = dao.createPassenger("Max", "Mustermann");
      
    And a flight LH-1234 executed on a Boing 737
      var closure = [IDBOperations command | 
        flight = command.createFlight("LH-1234");
        flight.setAirplane(command.createAirplaneType("Boing 737", 137));
      ];
      dao.executeInTransaction(closure);
      
    When a seat is booked
      success = dao.bookSeat(passenger, flight);
      
    Then a valid ticket is issued
      success => true

Aus jeder Feature-Beschreibung generiert Jnario automatisch einen JUnit-Test. Dieser enthält den entsprechenden Code und kann somit einfach in Eclipse ausgeführt werden. Jnario arbeitet an dieser Stelle im Gegensatz zu Cucumber mit einem Codegenerator statt mit einem Interpreter. Dieser Ansatz bietet den Vorteil, dass man den resultierenden Test-Code direkt inspizieren oder auch debuggen kann. Möchte man den generierten Code nicht unter Versionskontrolle stellen, aber die Tests auf einem CI-System ausführen, so muss dort der Jnario-Codegenerator ebenfalls ausgeführt werden. Dazu bietet Jnario ein entsprechendes Maven Plug-in an.

Jnario bietet ebenfalls die Möglichkeit, Features und Specs zu sogenannten Suites zu aggregieren und diese dann gemeinsam auszuführen. Des Weiteren hält Jnario einen Dokumentationsgenerator bereit, mit dem sich Specs in HTML-Dokumente überführen lassen. Diese HTML-Dokumente stellen dann den Inhalt der Spezifikation noch übersichtlicher dar. Technologisch basiert Jnario auf Xtend [6] und einem Editor, der mit Xtext [7] erstellt wurde. Dieser ist auch in Abb.1 zu sehen.

NatSpec

Zum Abschluss unseres Toolvergleichs wollen wir noch einen Blick auf das Werkzeug NatSpec werfen. NatSpec steht für "Natural Specification" und kommt aus dem Hause der DevBoost GmbH [8]. Das Werkzeug entstand ursprünglich im Rahmen eines Kundenprojekts und hatte als Zielgruppe Fachexperten mit eingeschränktem Know-how im Bezug auf Programmierung bzw. Programmiersprachen. Aus diesem Grund sollten die Testfälle nicht nur natürlichsprachlich festgehalten, sondern auch so flexibel wie irgend möglich gestaltet werden können. Aus diesen Anforderungen heraus geht NatSpec teilweise andere Wege als Cucumber oder auch Jnario, obwohl die Zielstellung aller Werkzeuge dieselbe ist.

NatSpec nutzt ähnlich wie Cucumber rein natürlichsprachliche Texte zur Spezifikation von Testfällen. Diese müssen jedoch keiner vorgegebenen Grundstruktur folgen, vielmehr kann beliebiger Text zur Definition von Szenarien verwendet werden. Vom Werkzeug selbst werden also weder feste Schlüsselworte noch andere Konventionen vorgegeben.

Der Support-Code wird, wieder ähnlich wie bei Cucumber, in speziellen Test-Support-Klassen abgelegt. Eine Einbettung in die Szenariobeschreibungen, wie bei Jnario, hätte die Nutzer von NatSpec irritiert, da diese weder Java noch Xtend beherrschten. NatSpec nutzt wie Cucumber Annotationen, um für Support-Methoden das zugehörige Muster festzulegen. Allerdings wird hier nicht auf reguläre Ausdrücke gesetzt, sondern auf eine einfachere Sprache, in der Platzhalter für die Methodenparameter lediglich durch ein Hashtag und den Index des Parameters repräsentiert werden. Der NatSpec-Support-Code für unser Beispiel ist in Listing 4 zu finden, die Spezifikation für das Testszenario zeigt Abb.2.

Listing 4:


public class TestSupport {
    
  private AirlineDAO dao;
  
  public TestSupport(AirlineDAO dao) {
    super();
    this.dao = dao;
  }
  
  @TextSyntax("Assert success")
  public void assertSuccess(boolean actual) {
    Assert.assertEquals(true, actual);
  }
  
  @TextSyntax("Create airplane #1")
  public AirplaneType createAirplane(String name) {
    return dao.createAirplaneType(name, 0);
  }
  
  @TextSyntax("Assume #2 seats")
  public AirplaneType setSeats(final AirplaneType plane, final int seats) {
    dao.executeInTransaction(new ICommand() {
        
      @Override
      public void execute(IDBOperations operations) {
        AirplaneType planeEntity = operations.getAirplaneType(plane.getId());
        planeEntity.setTotalSeats(seats);
      }
    });
    return plane;
  }
  
  @TextSyntax("Create flight #1")
  public Flight createFlight(final String name, final AirplaneType plane) {
    final Flight[] result = new Flight[1];
    dao.executeInTransaction(new ICommand() {
        
      @Override
      public void execute(IDBOperations operations) {
         Flight flight = operations.createFlight(name);
         flight.setAirplane(operations.getAirplaneType(plane.getId()));
         result[0] = flight;
      }
    });
    return result[0];
  }
  
  @TextSyntax("Book seat for #1 #2")
  public boolean bookSeat(String firstname, String lastname, Flight flight) {
    Passenger passenger = dao.createPassenger(firstname, lastname);
    return dao.bookSeat(passenger, flight);
  }
}

Durch die Nutzung der Annotation @TextSyntax wird es möglich, Texte noch flexibler als bei Cucumber auf Test-Code abzubilden. Die speziellen Zeichen für Zeilenanfang (^) und Zeilenende ($) entfallen gänzlich. NatSpec trifft hier die Annahme, dass Muster immer zeilenweise erkannt werden. Ist dies einmal nicht der Fall, so lässt sich diese Ausnahme von der Regel natürlich auch spezifizieren.

Bei genauer Betrachtung des Listings 4 fällt weiterhin auf, dass manche @TextSyntax-Annotationen nicht alle Parameter benutzen. So taucht beispielsweise bei setSeats() der zweite Parameter (#2) auf, der erste jedoch nicht. Hier kommt eine weitere Annahme von NatSpec zum Tragen, nämlich dass alle fehlenden Parameter aus dem Kontext der Spezifikation ergänzt werden. Wurde beispielsweise in einem vorhergehenden Schritt (s. Abb. 2) bereits ein Flugzeugtyp erzeugt, so wird dieser automatisch als Argument genutzt. Natürlich zeigt der NatSpec-Editor einen Fehler an, falls kein solches implizites Objekt vorhanden ist.

Durch die Nutzung der impliziten Parameter und der vollständig freien Syntax können Testfälle somit noch direkter mit natürlicher Sprache definiert werden. Der Autor eines Tests muss sich nicht mehr an vom Werkzeug vorgegebene Strukturen halten. Er kann die Szenarien so formulieren, wie es für das Projekt am sinnvollsten ist. Wie in Abb. 2 zu sehen ist, entstehen auf diese Weise sehr kompakte Texte. Insbesondere der automatische Bezug auf bereits erzeugte Objekte kommt dem menschlichen Denken sehr nahe, da man Aussagen im Normalfall immer auf den aktuellen Kontext, d. h. das zuletzt Gehörte oder Gelesene bezieht.

Um die Spezifikation von Akzeptanztests für Nutzer noch einfacher zu gestalten, unterstützt NatSpec Synonyme. So kann für jedes Wort eine Liste von Begriffen mit der gleichen Bedeutung festgelegt werden. Beim Abgleich der Muster und der Testanweisungen werden die Synonyme beachtet, so dass Testautoren sich nicht starr an die definierten Muster halten müssen. Diese Funktion ist u. a. bei der Verwendung von Substantiven in der Einzahl und Mehrzahl nützlich.

Der NatSpec-Editor prüft bei der Eingabe immer, ob für den Text passende Muster, d. h. Test-Support-Methoden, im Kontext des Eclipse-Projekts vorhanden sind. Wird ein passendes Muster gefunden, so werden die Worte im Satz entsprechend hervorgehoben. Die Schlüsselworte für die Testsyntax sind daher vollkommen dynamisch, die gewohnten Editor-Funktionalitäten wie Syntax-Highlighting, Code-Completion, Errors and Warnings stehen aber dennoch zur Verfügung. Anhand von Abb. 2 lässt sich gut erkennen, welche Worte in unserem Beispiel als Schlüsselworte, bzw. als Argumente erkannt wurden.

Neben den bereits genannten Basisfunktionalitäten kann NatSpec auch eine Dokumentation der verfügbaren Satzmuster generieren. Dies erleichtert Testautoren die Einarbeitung. Zudem wird eine API zur Verfügung gestellt um programmatisch Satzmuster zu erstellen. In dem eingangs beschriebenen Kundenprojekt wurden auf diese Weise Muster für eine komplette JPA-basierte Persistenzschicht erstellt. So konnte der Support-Code für diese Schicht erzeugt werden ohne mit großem Aufwand manuell Test-Support-Klassen zu schreiben.

Technologisch basiert NatSpec auf EMFText. Als Sprache für den Test-Support-Code kommt normales Java zum Einsatz. Natürlich kann hier auch jede andere kompatible Sprache (z. B. Xtend) genutzt werden. NatSpec generiert Tests aus den Testszenarien, d. h. es wird im Gegensatz zu Cucumber kein Interpreter genutzt. Dementsprechend steht auch bei NatSpec-Unterstützung für die gängigen Build-Systeme bereit.

Tabelle 1: Vergleich Cucumber, Jnario und NatSpec

  Cucumber Jnario NatSpec
Zuordnung Spezifikation zu Support-Code Annotations mit regulären Ausdrücken Direkte Einbettung in Spezifikation Annotations mit Platzhaltern für Parameter
Sprache für Support Code (auf JVM) Java Xtend Java
Testframework JUnit JUnit Flexibel
Syntax für Szenarien Gherkin Gherkin / Specification Syntax Frei definierbar
Dokumentationsgenerator   Szenarien Musterkatalog
Besonderheiten Mehrsprachigkeit Test Suites, Implicit Subjects, Dependency Injection MetaSpec API, Synonyme, Implizite Argumente
Lizenz MIT License EPL Kommerziell

Nachdem wir die Ansätze verschiedener Werkzeuge zur Unterstützung von ATDD nun etwas genauer betrachtet haben, wollen wir zum Abschluss noch einmal kurz auf drei Implikationen eingehen, die ein solches Vorgehen mit sich bringt:

  1. Der Fachexperte rückt bei ATDD mehr in den Vordergrund. Das ist genau das Ziel von ATDD, es ist jedoch wichtig, sich diesen Fakt vor Augen zu halten. Da die Szenarien einfacher zu lesen und zu verstehen sind, kann der Fachexperte aktiv partizipieren und im Zusammenspiel mit den Entwicklern und Testern die Szenarien erarbeiten. Eine solche Zusammenarbeit erfordert jedoch organisatorische Voraussetzungen. Können diese geschaffen werden, so wird die berüchtigte Kommunikationslücke zwischen den einzelnen Beteiligten drastisch reduziert.
  2. Testszenarien (zumindest im Fall von Cucumber und NatSpec) sind frei von technischen Details. Sie machen keinerlei Annahmen über die technische Umsetzung der Anwendung. Diese Abstraktion macht Sinn, da die Anwendung zum Zeitpunkt der Szenariodefinition u. U. noch gar nicht existiert und die Szenarien nicht geändert werden müssen, wenn sich die Anwendung weiter entwickelt. Natürlich muss im letzteren Fall der Test-Support-Code angepasst werden, aber immerhin bleiben die Anforderungen unverändert.
  3. Über die Generierung von ausführbaren Tests hinaus erhält man bei bestimmten ATDD-Werkzeugen auch wertvolle Dokumentation. Dies betrifft sowohl die Dokumentation für einzelne Szenarien als auch die Dokumentation für die verfügbaren Schritte zum Aufbau neuer Szenarien. Da diese Dokumentation automatisch generiert wird und somit immer aktuell ist, bekommt man wertvolle Artefakte geschenkt.

Betrachtet man das Vorgehen und die Werkzeuge bei ATDD in einem breiteren Blickwinkel, so erkennt man, dass die Spezifikation von Akzeptanztests nur ein Anwendungsfall ist. Die prinzipielle Idee, natürlichsprachliche Texte auf Code abzubilden und diesen auszuführen eröffnet zahlreiche weitere Möglichkeiten. So lässt sich beispielsweise das Domänenmodell einer Anwendung (z. B. die Entitäten und deren Beziehungen) auf die gleiche Art und Weise beschreiben. Dies ermöglicht es den Fachexperten, leichter zu verstehen, wie Entwickler fachliche Anforderungen technisch umsetzen. Letztendlich ist jedes Programm auf diese Art und Weise natürlichsprachlich spezifizierbar. Für Akzeptanztests liegen die Vorteile bereits auf der Hand. Welche weiteren Anwendungsfälle ebenso sinnvoll sind, wird sich noch zeigen müssen.

Quellen

[1] Cucumber
[2] Jnario 
[3] NatSpec
[4] Github-Projekt
[5] Github-Projekt
[6] eclipse.org: Xtend
[7] eclipse.org: Xtext
[8] DevBoost GmbH
 

Autoren

Dr. Tobias Nestler

Dr. Tobias Nestler ist COO der DevBoost GmbH. Vorher arbeitete er 6 Jahre als Senior Project Manager und Product Owner für die SAP AG im Rahmen internationalen R&D Projekte.
>> Weiterlesen

Dr. Christian Wende

Dr. Christian Wende ist Mitgründer und CEO der DevBoost GmbH. Er berät als Technologieexperte IT Unternehmen in Bereichen der Softwaremodernisierung, der Konzeption und Entwicklung von Produktlinien sowie dem Management und der…
>> Weiterlesen

Dr. Mirko Seifert

Dr. Mirko Seifert entwickelt seit mehr als 20 Jahren Software und ist aktiver Committer in verschiedenen Open-Source-Projekten (u.a. EMFText, JaMoPP, JUnitLoop). Er veröffentlichte zahlreiche Artikel im Eclipse- und im…
>> Weiterlesen
Kommentare (0)

Neuen Kommentar schreiben