Suche
Es lebe das Testen

Die Highlights von JUnit 5: Neues für Java-Tester

Matthias Merdes

© Shutterstock / sergey pozhoga

Das Testframework JUnit hat seit dem ersten Release wesentlich dazu beigetragen, testgetriebene Entwicklung voranzubringen. Der Legende nach wurde es ursprünglich im Jahr 1997 von Kent Beck und Erich Gamma im Flugzeug per Pair Programming entwickelt [1] und war seitdem Vorbild für viele andere Testframeworks. Nach langer Entwicklung ist mit JUnit 5 endlich das ersehnte Major-Release erschienen. Wir werfen einen Blick auf die wichtigsten neuen Features.

Im Laufe der Jahre hatte es sich gezeigt, dass die zur Verfügung stehenden Konstrukte von JUnit Erweiterungen erschwerten. Auch die Art und Weise, in der IDEs JUnit integrierten, stellte sich als zunehmend hinderlich für die Weiterentwicklung heraus. Dazu kommt, dass die vorherigen Versionen keine Sprachfeatures von Java 8 unterstützen konnten, weil stets die Kompatibilität zu Java 5 gewahrt bleiben musste. Nach längerer Zeit ohne neue Releases und ohne grundlegende Modernisierungsmöglichkeit hat im Oktober 2015 die Entwicklung der vollständig überarbeiteten Version 5 begonnen. Nach einer Startfinanzierung durch die Crowdfunding-Kampagne JUnit Lambda begann im Oktober 2015 die Planung der neuen Version bei einem Workshop mit dem Core-Committer-Team und Herstellern der Tools Eclipse, Intellij IDEA, Gradle und Spring. Nach einem Prototyp und einer Reihe von Milestone-Releases ist im Sommer 2017 mit Version 5.0.0 GA das erste Produktionsrelease der neuen Version erschienen. Version 5.1 ist bereits in Planung.

Ich stelle einige neue Features der neuen Version 5 vor, die für das Schreiben von Tests und damit für alle Entwickler wichtig sind. Als Einstieg soll ein minimalistisches Beispiel dienen, bevor ich einige fortgeschrittene oder grundsätzlich neue Feature vorstelle. Alles Folgende bezieht sich auf die neue JUnit-Jupiter-Test-Engine. Wie es nahe liegt, heißt die wichtigste Annotation immer noch @Test. Im Unterschied zu JUnit 4 muss man allerdings ein anderes Package importieren: org.junit.jupiter.api anstelle von org.junit. Hierbei bezeichnet jupiter die Test-Engine, die das neue JUnit-5-Programmiermodell unterstützt. Die neue JUnit-Plattform unterstützt die Koexistenz verschiedener Test-Engines.

@BeforeALL
static void setupServer() {}

@BeforeEach
void prepareDatabase() {}

@Test
void myTest() {}

@AfterEach
void cleanupDatabase() {}

@AfterALL
static void tearDownServer {}

Man erkennt in Listing 1, dass sich die Namen der Lifecycle-Methoden im Vergleich zu JUnit 4 geändert haben. Die neue Nomenklatur ist das Ergebnis langer Diskussionen und versucht, die Bedeutung noch deutlicher hervorzuheben, als es bisher der Fall war [2]. @BeforeAll wird einmal pro Testklasse aufgerufen, @BeforeEach vor jeder einzelnen Testmethode. Gleiches gilt für die After-Methoden. Erwähnenswert ist ebenfalls, dass der Modifier public entfallen kann.

Frei definierbare Namen

Die Annotation @DisplayName ermöglicht es, fast beliebige Namen für Testklassen und einzelne Testmethoden zu verwenden, sodass sich übersichtliche und gut lesbare Baumdarstellungen im GUI der IDE aufbauen lassen (Listing 2).

@DiplayName("HTTP tests for REST product service")
public class DisplayNameDemo {
  
  @Test
  @Displayname("GET 'http://localhost:8080/products/4711' user: Bob")
  public void getProduct() {...}
  ...
  @Test
  @DisplayName("POST 'http://localhost:8080/products/' user:Alice")
  public void addProduct() {...}
 

Dies ergibt eine saubere Darstellung in der IDE (Abb. 1). Das Besondere an diesen neuen Features ist die Tatsache, dass die IDE trotzdem noch in der Lage ist, zwischen der Baumansicht und dem Sourcecode zu navigieren. Das wird dadurch ermöglicht, dass die JUnit-Plattform der IDE die Möglichkeit gibt, einzelne Tests sauber zu referenzieren, ohne reflexiv auf den Methodennamen zuzugreifen.

Abb. 1: Ausführung von Tests mit frei definierte Namen

Parameter Injection und Test-Reporting

Eine weitere Neuerung, die vor allem aus dem Spring Framework und anderen Dependency-Injection-Mechanismen bekannt ist, ist die Möglichkeit der Parameter-Injection.

@Test
void reporting(TestReporter reporter) {
reporter.publishEntry("balance", "47.11");
}

Durften Testmethoden in den bisherigen Versionen von JUnit keine Argumente haben, fällt diese Einschränkung mit JUnit Jupiter weg. Dieser neue Mechanismus ermöglicht es Erweiterungen, Parameter für die Ausführung einer Testmethode bereitzustellen. Dazu können ParameterResolver-Extensions registriert werden, um Parameter aufzulösen, z. B. basierend auf einem bestimmten Typ oder einer bestimmten Annotation. Im Beispiel wird der bereits mitgelieferte typbasierte TestReporterParameterResolver aktiv, um eine TestReporter-Instanz zu liefern. Mit diesem TestReporter können unabhängig von der Konsole Informationen geordnet an die ausführende Umgebung geliefert werden.

Tags und Metaannotationen

Eine weitere Veränderung im Vergleich zu JUnit 4 ist das Tagging von Tests mit bestimmten Labeln, was in JUnit 5 String-basiert und nicht mehr über den klassenbasierten Category-Mechanismus gelöst ist.

@Test
@Tag("fast")
public void taggedTest() {...}

Möchte man nun für eine Menge von Tests, z. B. für alle Integrationstests, ein solches Tag vergeben, kann es sinnvoll sein, einen neu eingeführten Mechanismus zu nutzen. So wird nämlich für alle im Jupiter-API vorhandenen Annotationen eine Kombination von Annotationen zu Metaannotationen unterstützt. Ähnliche Mechanismen sind aus dem Spring Framework oder auch aus der Programmiersprache Groovy bekannt.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}

Im Beispiel werden besonders schnell ausführbare Tests durch die Metaannotation @FastTest gekennzeichnet, z. B. weil man sie aus Fast-Feedback-Gründen im Build zuerst ausführen möchte. Mit diesem Mechanismus lassen sich Möglichkeiten des Jupiter-API parametrisieren und zu höherwertigen Abstraktionen zusammenfassen. Das kann der konsistenten Verwendung durch verschiedene Testautoren im Team dienen.

Nested Tests

JUnit Jupiter unterstützt direkt das Erstellen von geschachtelten Tests über die Annotation @Nested (Listing 3). Diese Fähigkeit ist grob vergleichbar mit dem aus JUnit 4 bekannten HierarchicalContextRunner.

@DisplayName("A stack")
public class TestingAStack {

  Stack<Object> stack;

  @Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() { new Stack<>(); }

@Nested
@DisplayName("when new")
class WhenNew {

  @BeforeEach
  void init() { stack = new Stack<>; }

  @Test
  @DisplayName("is empty")
  void isEmpty() { Assertions.assertTrue(stack.isEmtpy());}
 

Auch hier führt die Ausführung zu einem visuell ansprechenden Ergebnis in der IDE, das in gewisser Weise schon an die spezifikationsnahen Darstellungen aus BDD-Frameworks erinnert (Abb. 2).

Abb. 2: Ausführung von Nested Tests in der IDE

Dynamische Tests: @TestFactory und @ParameterizedTest

Während die grundlegenden Annotationen wie @Test, @BeforeEach oder @AfterAll im Wesentlichen eine vergleichbare Semantik wie die entsprechenden JUnit-4-Vorgänger haben, gibt es in JUnit 5 auch grundlegend neue Konzepte. Hierzu zählt insbesondere die Möglichkeit, dynamische Tests zu erstellen. Neben den schon aus JUnit 4 in verschiedenen Varianten bekannten parametrisierten Tests ist vor allem die Annotation @TestFactory eine besondere Neuerung. Mit dieser Annotation gekennzeichnete Methoden sind selbst keine Tests, sondern liefern einen Stream oder eine Collection an Testfällen zurück. Dieser Stream entsteht erst zur Laufzeit, seine Länge ist nicht vorab bekannt. Durch die in der JUnit-Plattform exponierten Listener ist die IDE aber dennoch in der Lage, für jeden Testfall innerhalb des Streams einen sauberen Knoten im Ausführungsbaum darzustellen – in Verbindung mit dynamischen Namen. Lässt man z. B. das Beispiel aus Listing 4 in einer IDE ablaufen, sieht man, dass die einzelnen Knoten tatsächlich erst zur Laufzeit bei der Durchführung des gerade entstandenen Tests zur grafischen Darstellung hinzugefügt werden.

import static org.junit.jupiter.api.DynamicTest.dynamicTest;

class DynamicFibonacciDemo {

  @TestFactory
  @DisplayName("all Fibonaccis are odd")
  Stream<DynamicTest> allFibonaccisAreOdd() {
    return IntStream.range(1, 13).boxed()
      .map(this::fibonacci)
      .map(number -> dynamicTest(displayName "Fibonacci = " + number, () -> isOdd(number))
      );
}
 

Im Beispiel geht man von einem Stream natürlicher Zahl aus und bildet diesen mithilfe der Methodenreferenz fibonacci auf Fibonacci-Zahlen ab. Diese Fibonacci-Folge wird dann über das statisch importierte org.junit.jupiter.api.DynamicTest.dynamicTest(String displayName, Executable executable) auf einen Stream von dynamischen Testfällen abgebildet. Auch wenn in diesem Beispiel bereits zum Zeitpunkt der Testerstellung die Folge auf zwölf Testfälle begrenzt wird, könnte man jederzeit ein erst zur Laufzeit berechnetes Abbruchkriterium verwenden (Abb. 3).

Abb. 3: Ausführung dynamischer Tests in der IDE

Eine solche Funktionalität lässt sich mithilfe von Lambdaausdrücken elegant realisieren. Die so gewonnene Ausdrucksmächtigkeit für dieses und andere Features war bei den Vorüberlegungen für das neue JUnit 5 im Jahr 2015 mit ausschlaggebend dafür, die Abwärtskompatibilität zu Java 5 fallen zu lassen. Nicht umsonst trug die zur Anschubfinanzierung der Neuentwicklung initiierte Crowdfunding-Kampagne den Namen JUnit Lambda.

Erwähnenswert ist in diesem Zusammenhang, dass es für diese Art von dynamischen Tests explizit keine Ausführung von Lifecycle-Methoden auf der Ebene einzelner Testfälle gibt, sondern nur für die ganze @TestFactory-Methode. Möchte man also, dass z. B. @BeforeEach-Methoden für jede einzelne einer Menge von Testfällen aufgerufen werden, sollte man die Annotation @ParameterizedTest verwenden. Diese Art von Test bietet außerdem den Vorteil, dass die Werte, die die einzelnen Testfälle parametrisieren, aus diversen vorhandenen (z. B. CsvSource oder EnumSource) oder selbst implementierbaren Quellen bezogen werden können. Im Gegensatz dazu werden diese Werte bei den zuvor behandelten dynamischen Tests typischerweise direkt im Code berechnet.

In unserem Beispiel werden die Parameter der Einfachheit halber mithilfe der Annotation @MethodSource aus einer lokalen Methode gelesen (Listing 5). @ParameterizedTest ist eine Anwendung des allgemeineren TestTemplate-Konzepts, mit dessen Hilfe auch das verwandte @RepeatedTest umgesetzt ist (Abb. 4).

 @ParameterizedTest
@Methodsource("providerMethod")
void testWithParametersFromMethods(String parameter) {
  assertEquals(expected: "two", parameter);
}
...
static Iterable<String> providerMethod() {
  return asList("one", "two", "three");
}

Zusammenfassend lässt sich sagen, dass beide Varianten es ermöglichen, eine Gruppe von Testfällen zu erzeugen, deren Anzahl zuvor nicht bekannt ist. Die Auswahl einer Variante lässt sich dadurch treffen, ob man Lifecycle-Support auf Testfallebene benötigt und ob die parametrisierenden Werte sich im Test leicht berechnen lassen oder aus externen Quellen bezogen werden müssen.

Abb. 4: Ausführung parametrisierter Tests in der IDE

Architektur: Platform, Test-Engines und Extensions

Eines der Hauptziele bei der Neuentwicklung von JUnit 5 war die logische Trennung der Rollen zum Verfassen eines Tests und zur Ausführung von Tests. Physisch spiegelt sich das in der Entkopplung der APIs zum Schreiben von denen zum Ausführen von Tests wider. In JUnit 4 sind beide APIs in einem Artefakt enthalten. Alles, was JUnit auf irgendeine Weise verwendet, hängt von dieser einzigen JAR-Datei ab, unabhängig davon, ob es sich dabei um IDEs, Build Tools, andere Testframeworks oder Testcode handelt. Diese nutzen teilweise Interna von JUnit, wie etwa interne Klassen oder private Instanzvariablen. So scheiterte z. B. der Versuch, die Version 4.12-beta-1 in Kombination mit einer bekannten IDE zu betreiben, weil die IDE eine private Variable, die umbenannt worden war, per Reflection referenziert hatte. Diese Art der Integration war der Tatsache geschuldet, dass es eben kein ausreichend mächtiges API für die Integration in Tools gab. Die Weiterentwicklung von JUnit 4 wurde dadurch immens erschwert, in Teilen sogar nahezu unmöglich gemacht.

Abb. 5: Gesamtarchitektur von JUnit 5

Wie Abbildung 5 zeigt, wurde beim Entwurf von JUnit 5 großer Wert darauf gelegt, eine möglichst saubere Trennung der APIs für die unterschiedlichen Rollen der verschiedenen Tools zu erreichen. So benötigt ein Testautor lediglich die Dependency junit-jupiter-api, die alle Annotationen wie etwa @Test, Lifecycle-Annotationen und grundlegende Assertions mitbringt. Tools, die eine JUnit-5-Integration anbieten wollen, wie etwa IDEs oder Build-Tools, verwenden das Modul junit-platform-launcher innerhalb der JUnit-5-Plattform, um die Ausführung von Tests anzustoßen. Der darin enthaltene Launcher orchestriert das Auffinden und die Ausführung von Tests durch verschiedene Engines.

Lesen Sie auch: JUnit 5: So bastelt man seine eigene Test-Engine

Die Engine-Abstraktion ermöglicht die Koexistenz verschiedener Testframeworks und -bibliotheken, und damit insbesondere auch die Ausführung von Testfällen auf Basis von JUnit 4 und JUnit 5 innerhalb eines einzigen Testlaufs. Das impliziert, dass man bei existierenden Projekten auf Basis von JUnit 4 langsam mit einigen JUnit-5-Tests beginnen kann und keineswegs gezwungen ist, eine Big-Bang-Migration durchzuführen. Weitere Hilfen für die Migration, wie etwa der Support ausgewählter Rules, finden sich im Artefakt junit-jupiter-migrationsupport. Um von einem integrierenden Tool angesprochen zu werden, müssen Test-Engines lediglich auf dem Klassenpfad vorhanden sein und sich über den normalen ServiceLoader-Mechanismus der JVM [3] beim Launcher registrieren.

Die Engine-Implementierungen hängen dabei nur vom Artefakt junit-platform-engine ab. JUnit 5 liefert zwei Engines aus: Die junit-vintage-engine dient dem Auffinden und der Ausführung von JUnit-4-Tests. Die junit-jupiter-engine hingegen implementiert das neue Programmiermodell von JUnit 5. Das Engine-API ist dabei im Prinzip ein Angebot an existierende und zukünftige Testframeworks. Solche Frameworks müssen nur eine Engine-Implementierung bereitstellen und profitieren sofort von der Integration, die die Toolhersteller für die JUnit-5-Plattform einmalig erstellt haben. Da eine Engine-Implementierung in vielen Fällen recht einfach zu erstellen ist [4], sinkt somit die Schwelle, ein sauber integriertes Testframework einem weiteren Benutzerkreis zur Verfügung zu stellen. Einige Projekte haben bereits gegen Milestone-Releases von JUnit 5 eigene Engines implementiert, z. B. die jqwik-Engine für Property-based Testing [5] oder das BDD-Framework Specsy [6]. Zusammenfassend lässt sich sagen, dass das neue JUnit 5 erstmalig eine Plattformabstraktion zur Integration bereitstellt, die sauber von Test-Engines getrennt ist, die ihrerseits ein bestimmtes Programmiermodell für Tests unterstützen. Die JUnit Jupiter Engine realisiert dabei das neue JUnit-5-Programmiermodell, die JUnit Vintage Engine wird aus Gründen der Abwärtskompatibilität und Migrationsunterstützung mitgeliefert.

Extensions

Im Gegensatz zu den zuvor beschriebenen Test-Engines, die es ermöglichen, beliebige Testdefinitionen und Ausführungssemantiken zu unterstützen, dient das Extension-Modell zur Verfeinerung der Jupiter-Engine. Das hybride Erweiterungsmodell von JUnit 4 aus Runner und Rule hatte sich über die Jahre hinweg als problematisch herausgestellt. Daher wurde bei JUnit 5 viel Energie darauf verwendet, ein sowohl einheitliches als auch ausdrucksstarkes Erweiterungsmodell für die Jupiter Engine zu entwerfen und umzusetzen. Es handelt sich dabei um ein feingranulares Erweiterungsmodell, das einen selektiven Eingriff an vielen Stellen der Testdefinition und -ausführung erlaubt. Insbesondere lassen sich die Erweiterungen aus technischer Sicht beliebig kombinieren.

Als Basis aller Erweiterungen für JUnit Jupiter gibt es das Marker-Interface Extension. Nehmen wir nun an, dass eine einfache Extension zur Integration des Mock-Frameworks Mockito erstellt werden soll. Konkret müsste eine solche MockitoExtension die beiden ExtensionPoints TestInstancePostProcessor und ParameterResolver implementieren, wenn man die Extension dann wie in Abbildung 6 verwenden will. Beide ExtensionPoints erweitern wiederum das Marker-Interface (Listing 6).

 @ExtendedWith(MockitoExtension.class)
class ExtensionTestDemo {

  @BeforeEach
  Void setup (@Mock List<String> list) {...}
  ...
  @Test
  void aTest(@Mock List<String> list) {...}

}

Im Beispiel aus Listing 6 wird der ExtensionPoint TestInstancePostProcessor benötigt, um über das Mockito-API die Mocks direkt nach dem Erstellen der Testinstanz zu initialisieren. Der ExtensionPoint ParameterResolver hingegen dient dazu, die so erzeugten Mocks elegant in die gewünschten Test- oder Lifecycle-Methoden hineinreichen zu können. Damit lässt sich präzise ausdrücken, für welche Methoden welche Mocks bestimmt sind.

Abb. 6: Klassendiagramm zu Extensions

Neben den beiden hier genannten ExtensionPoints gibt es eine ganz Reihe anderer, etwa zur Handhabung von Ausnahmen und zur Interaktion mit den benutzerdefinierten Methoden des Test-Lifecycles, z. B. @BeforeAll und BeforeAllCallback. Benötigt man in einer Extension einen Zustand über verschiedene Aufrufe hinweg, sollte man diesen im bereitgestellten ExtensionContext.Store ablegen, da die Zustandslosigkeit von Erweiterungen wichtig für deren Komponierbarkeit ist. Man kann also entweder auf einer sehr hohen Granularitätsebene eine eigene Test-Engine implementieren oder aber die Standard-Test-Engine Jupiter durch feingranulare und komponierbare Extensions erweitern. Eine Kombination aus eigener Engine und Jupiter Extensions funktioniert auch in JUnit 5 nicht. Das liegt aber in der Natur der Sache, da ein Erweiterungsmodell notwendigerweise die Abstraktionen einer konkreten Engine manipulierbar machen muss und sich nicht sinnvoll über alle denkbaren Engines hinweg verallgemeinern lässt.

Integration und Kompatibilität

Wesentlich für den Erfolg eines Testframeworks sind nicht nur moderne oder elegante Features, sondern vor allem auch die Integration in andere Werkzeuge. Mindestens ebenso wichtig wie das Ausführung von Tests innerhalb einer IDE ist sicherlich die Integration mit CLI-basierten Build-Tools, die in CI-Builds zum Einsatz kommen. Die meisten Projekte im Java-Umfeld werden heute sicherlich mit Maven oder Gradle gebaut. Daher hat das JUnit-5-Team von Anfang an ein Gradle-Plug-in und einen Maven-Surefire-Provider mitentwickelt. Der Surefire-Provider wurde bereits an das entsprechende Apache-Projekt übergeben, um dort weiterentwickelt zu werden. Für das Gradle-Plug-in ist ein ähnliches Vorgehen in Vorbereitung. Möchte man ein JUnit-5-Projekt mit Maven oder Gradle neu aufsetzen, lohnt sich ein Blick auf die entsprechenden Beispielprojekte [8].

Mehr zum Thema: Testen mit Docker und jUnit5

Zusätzlich zu dieser Build-Tool-Integration gibt es einen ConsoleLauncher, der sich mit zahlreichen Optionen parametrisieren lässt und das Ergebnis einer Testausführung in einer gut formatierten Baumdarstellung ausgibt. Dazu gibt es im zentralen Maven-Repository unter der Dependency junitplatform-console-standalone ein Stand-alone-Executable, das direkt mit java -jar aufgerufen werden kann. Mit diesem Werkzeug wäre auch eine Integration in nicht direkt unterstützte Build-Tools wie Ant möglich.

JUnit 4 und 5 (Jupiter)

Wie bereits im Abschnitt uber die Architektur erwähnt, war es ein wesentliches Designziel, mehrere Engines in einem einzigen Lauf gleichzeitig ausführen zu konnen. Dies gilt naturlich insbesondere für die Koexistenz von JUnit 4- und Junit Jupiter-Tests. Für die Ausführung von JUnit-4-Tests innerhalb einer neuen JUnit-5-Umgebung existiert die bereits beschriebene JUnit Vintage Engine. Umgekehrt lassen sich neue Jupiter-Tests in JUnit-4-Umgebungen ausführen, wenn man den Runner org.junit.platform.runner.JUnitPlatform aus der Dependency junit-platform-runner verwendet, der das Jupiter-Programmiermodell bestmöglich auf die in JUnit 4 vorhandenen Abstraktionen übersetzt. Auch wenn beide Integrationen eher nicht für eine dauerhafte Verwendung vorgesehen sind, lassen sich doch einige Migrationsjahre damit bestreiten.

Zusammenarbeit mit Java 9

Seit Version 5.0.0 kann JUnit 5 zusammen mit Java 9 verwendet werden. Die CI-Builds laufen schon seit einigen Monaten sowohl gegen Java 8 als auch Java 9. Die Modulinformation wird derzeit noch automatisch generiert, d. h. dass im Moment noch alle Klassen physisch verwendbar sind. Um für die Zukunft gewappnet zu sein, sollte man als Anwender jedoch bereits jetzt auf die Verwendung von Klassen verzichten, die mit @API(Status.INTERNAL) annotiert sind.

Fazit

An ein Testframework werden hohe Anforderung in Bezug auf Stabilität und Kompatibilität gestellt, das gilt insbesondere für ein so lange existierendes und so weit verbreitetes Framework wie JUnit. Das JUnit-5-Team hat von Anfang versucht, diese besonderen Anforderungen in Einklang mit den nicht zuletzt durch Java 8 ermöglichten Innovationen zu bringen. Wichtiger noch als eine größere Eleganz bei der Testdefinition oder die vereinfachte Entwicklung von Erweiterungen ist möglicherweise sogar die grundlegend neu konzipierte Architektur, die erstmals zwischen allgemeiner und stabiler Plattform einerseits und verschiedenen Engines andrerseits unterscheidet. Nicht zuletzt ermöglicht die Plattform eine saubere und mächtige Einbindung beliebter Engines in IDEs und Build-Tools. Ein großes Dankeschön geht an die Kollegen Marc Philipp, Sam Brannen, Christian Stein und Stefan Bechtold vom JUnit-5-Team sowie an Johannes Link, der nicht nur diesen Artikel Korrektur gelesen, sondern vor allem auch die Überlegungen zur grundlegend neuen Architektur wesentlich mitgestaltet hat. Dank gebührt auch den vielen frühen Anwendern, die die Entwicklung von JUnit 5 in den letzten beiden Jahren durch Feedback und Diskussionen begleitet haben.

HINTERGRUND ZUM THEMA:

Testen ohne neu zu bauen: Java-8-Features in JUnit-Tests

 

Geschrieben von
Matthias Merdes
Matthias Merdes
Matthias Merdes ist Lead Developer Architecture and Services bei der Heidelberg Mobil International GmbH, einem der Hauptsponsoren von JUnit Lambda. Er befasst sich seit JDK 1.1 mit Java-Technologien vor allem im Backend-Bereich und ist begeisterter Groovy-Programmierer. Allgemein interessiert sich Matthias für alle Technologien, die Enterprise Development einfacher, eleganter und effizienter machen.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.