Ein Leitfaden für robuste Unit- und Integrationstests mit JUnit

Veröffentlicht: 2022-03-11

Automatisierte Softwaretests sind von entscheidender Bedeutung für die langfristige Qualität, Wartbarkeit und Erweiterbarkeit von Softwareprojekten, und für Java ist JUnit der Weg zur Automatisierung.

Während sich der größte Teil dieses Artikels auf das Schreiben robuster Unit-Tests und die Verwendung von Stubbing, Mocking und Dependency Injection konzentriert, werden wir auch JUnit- und Integrationstests besprechen.

Das JUnit-Testframework ist ein allgemeines, kostenloses und Open-Source-Tool zum Testen von Java-basierten Projekten.

Zum jetzigen Zeitpunkt ist JUnit 4 die aktuelle Hauptversion, die vor mehr als 10 Jahren veröffentlicht wurde, wobei das letzte Update mehr als zwei Jahre zurückliegt.

JUnit 5 (mit den Jupiter-Programmier- und Erweiterungsmodellen) befindet sich in aktiver Entwicklung. Es unterstützt die in Java 8 eingeführten Sprachfunktionen besser und enthält weitere neue, interessante Funktionen. Einige Teams finden JUnit 5 möglicherweise einsatzbereit, während andere möglicherweise weiterhin JUnit 4 verwenden, bis 5 offiziell veröffentlicht wird. Wir werden uns Beispiele aus beiden ansehen.

Ausführen von JUnit

JUnit-Tests können direkt in IntelliJ ausgeführt werden, aber sie können auch in anderen IDEs wie Eclipse, NetBeans oder sogar der Befehlszeile ausgeführt werden.

Tests sollten immer zur Build-Zeit ausgeführt werden, insbesondere Unit-Tests. Ein Build mit fehlgeschlagenen Tests sollte als fehlgeschlagen angesehen werden, unabhängig davon, ob das Problem im Produktions- oder im Testcode liegt – dies erfordert vom Team Disziplin und die Bereitschaft, der Lösung fehlgeschlagener Tests höchste Priorität einzuräumen, aber es ist notwendig, sich daran zu halten Geist der Automatisierung.

JUnit-Tests können auch von Continuous-Integration-Systemen wie Jenkins ausgeführt und gemeldet werden. Projekte, die Tools wie Gradle, Maven oder Ant verwenden, haben den zusätzlichen Vorteil, dass sie Tests als Teil des Build-Prozesses ausführen können.

Gruppen von Zahnrädern, die Kompatibilität anzeigen: JUnit 4 mit NetBeans in einem, JUnit 5 mit Eclipse und Gradle in einem anderen und das letzte mit JUnit 5 mit Maven und IntelliJ IDEA.

Gradl

Ein Gradle-Beispielprojekt für JUnit 5 finden Sie im Gradle-Abschnitt des JUnit-Benutzerhandbuchs und im Repository junit5-samples.git. Beachten Sie, dass es auch Tests ausführen kann, die die JUnit 4-API verwenden (als „vintage“ bezeichnet).

Das Projekt kann in IntelliJ über die Menüoption File > Open… erstellt werden > navigieren Sie zum junit-gradle-consumer sub-directory > OK > Open as Project > OK, um das Projekt aus Gradle zu importieren.

Für Eclipse kann das Buildship Gradle-Plug-in über Help > Eclipse Marketplace… installiert werden. Das Projekt kann dann mit File > Import… > Gradle > Gradle Project > Next > Next > Browse to the junit-gradle-consumer sub-directory > Next importiert werden > Weiter > Fertig.

Nach dem Einrichten des Gradle-Projekts in IntelliJ oder Eclipse umfasst das Ausführen des Gradle- build -Tasks das Ausführen aller JUnit-Tests mit dem test . Beachten Sie, dass die Tests bei nachfolgenden Ausführungen von build möglicherweise übersprungen werden, wenn keine Änderungen am Code vorgenommen wurden.

Für JUnit 4 siehe Verwendung von JUnit mit Gradle im Wiki.

Maven

Für JUnit 5 finden Sie im Abschnitt Maven des Benutzerhandbuchs und im Repository junit5-samples.git ein Beispiel für ein Maven-Projekt. Dies kann auch Vintage-Tests ausführen (solche, die die JUnit 4-API verwenden).

Verwenden Sie in IntelliJ File > Open… > navigieren Sie zu junit-maven-consumer/pom.xml > OK > Open as Project. Die Tests können dann über Maven Projects > junit5-maven-consumer > Lifecycle > Test ausgeführt werden.

Verwenden Sie in Eclipse File > Import… > Maven > Existing Maven Projects > Next > Browse to the junit-maven-consumer directory > With the pom.xml selected > Finish.

Die Tests können ausgeführt werden, indem das Projekt als Maven-Build ausgeführt wird… > Ziel des test angeben > Ausführen.

Für JUnit 4 siehe JUnit im Maven-Repository.

Entwicklungsumgebungen

Zusätzlich zum Ausführen von Tests über Build-Tools wie Gradle oder Maven können viele IDEs direkt JUnit-Tests ausführen.

IntelliJ-IDEE

IntelliJ IDEA 2016.2 oder höher ist für JUnit 5-Tests erforderlich, während JUnit 4-Tests in älteren IntelliJ-Versionen funktionieren sollten.

Für die Zwecke dieses Artikels möchten Sie möglicherweise ein neues Projekt in IntelliJ aus einem meiner GitHub-Repositories ( JUnit5IntelliJ.git oder JUnit4IntelliJ.git) erstellen, das alle Dateien im einfachen Person -Klassenbeispiel enthält und das integrierte JUnit-Bibliotheken. Der Test kann mit Run > Run 'All Tests' ausgeführt werden. Der Test kann auch in IntelliJ aus der PersonTest -Klasse ausgeführt werden.

Diese Repositorys wurden mit neuen IntelliJ-Java-Projekten erstellt und bauen die Verzeichnisstrukturen src/main/java/com/example und src/test/java/com/example auf. Das Verzeichnis src/main/java wurde als Quellordner angegeben, während src/test/java als Testquellordner angegeben wurde. Nachdem die PersonTest -Klasse mit einer mit @Test annotierten Testmethode erstellt wurde, kann sie möglicherweise nicht kompiliert werden. In diesem Fall bietet IntelliJ den Vorschlag an, JUnit 4 oder JUnit 5 zum Klassenpfad hinzuzufügen, der aus der IntelliJ IDEA-Distribution geladen werden kann (siehe diese Antworten auf Stack Overflow für weitere Details). Schließlich wurde eine JUnit-Laufkonfiguration für alle Tests hinzugefügt.

Siehe auch die IntelliJ Testing How-to Guidelines.

Finsternis

Ein leeres Java-Projekt in Eclipse hat kein Teststammverzeichnis. Dies wurde über Projekteigenschaften > Java-Erstellungspfad > Ordner hinzufügen… > Neuen Ordner erstellen… > Ordnernamen angeben > Fertig stellen hinzugefügt. Das neue Verzeichnis wird als Quellordner ausgewählt. Klicken Sie in beiden verbleibenden Dialogfeldern auf OK.

JUnit 4-Tests können mit Datei > Neu > JUnit-Testfall erstellt werden. Wählen Sie „Neuer JUnit 4-Test“ und den neu erstellten Quellordner für Tests aus. Geben Sie eine „getestete Klasse“ und ein „Paket“ an und stellen Sie sicher, dass das Paket mit der zu testenden Klasse übereinstimmt. Geben Sie dann einen Namen für die Testklasse an. Wählen Sie nach Abschluss des Assistenten, wenn Sie dazu aufgefordert werden, „JUnit 4-Bibliothek hinzufügen“ zum Erstellungspfad. Das Projekt oder die einzelne Testklasse kann dann als JUnit-Test ausgeführt werden. Siehe auch Eclipse Schreiben und Ausführen von JUnit-Tests.

NetBeans

NetBeans unterstützt nur JUnit 4-Tests. Testklassen können in einem NetBeans Java-Projekt mit File > New File… > Unit Tests > JUnit Test oder Test for Existing Class erstellt werden. Standardmäßig heißt das Stammverzeichnis test im Projektverzeichnis test.

Einfache Produktionsklasse und ihr JUnit-Testfall

Werfen wir einen Blick auf ein einfaches Beispiel für Produktionscode und den entsprechenden Komponententestcode für eine sehr einfache Person -Klasse. Sie können den Beispielcode aus meinem Github-Projekt herunterladen und über IntelliJ öffnen.

src/main/java/com/example/Person.java
 package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }

Die unveränderliche Person -Klasse hat einen Konstruktor und eine getDisplayName() Methode. Wir wollen testen, getDisplayName() den Namen so formatiert zurückgibt, wie wir es erwarten. Hier ist Testcode für einen einzelnen Komponententest (JUnit 5):

src/test/java/com/example/PersonTest.java
 package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }

PersonTest verwendet @Test und Assertion von JUnit 5. Für JUnit 4 müssen die PersonTest -Klasse und -Methode öffentlich sein und es sollten verschiedene Importe verwendet werden. Hier ist das JUnit 4-Beispiel Gist.

Beim Ausführen der PersonTest -Klasse in IntelliJ sind die Testdurchläufe und die UI-Anzeigen grün.

Gemeinsame JUnit-Konventionen

Benennung

Obwohl dies nicht erforderlich ist, verwenden wir allgemeine Konventionen bei der Benennung der Testklasse. Insbesondere beginnen wir mit dem Namen der zu testenden Klasse ( Person ) und hängen „Test“ daran an ( PersonTest ). Die Benennung der Testmethode ist ähnlich, beginnend mit der zu testenden Methode ( getDisplayName() ) und dem vorangestellten „test“ ( testGetDisplayName() ). Während es viele andere vollkommen akzeptable Konventionen für die Benennung von Testmethoden gibt, ist es wichtig, im gesamten Team und Projekt konsistent zu sein.

Name in der Produktion Name im Test
Person Personentest
getDisplayName() testDisplayName()

Pakete

Wir wenden auch die Konvention an, die PersonTest -Klasse des Testcodes im selben Paket ( com.example ) wie die Person -Klasse des Produktionscodes zu erstellen. Wenn wir ein anderes Paket für Tests verwenden würden, müssten wir den Modifikator für den öffentlichen Zugriff in Klassen, Konstruktoren und Methoden des Produktionscodes verwenden, auf die von Komponententests verwiesen wird, auch wenn dies nicht angemessen ist. Daher ist es besser, sie einfach im selben Paket zu belassen . Wir verwenden jedoch separate Quellverzeichnisse ( src/main/java und src/test/java ), da wir im Allgemeinen keinen Testcode in veröffentlichte Produktions-Builds aufnehmen möchten.

Struktur und Anmerkung

Die Annotation @Test (JUnit 4/5) weist JUnit an, die Methode testGetDisplayName() als Testmethode auszuführen und zu melden, ob sie bestanden oder fehlgeschlagen ist. Solange alle Zusicherungen (falls vorhanden) erfolgreich sind und keine Ausnahmen ausgelöst werden, gilt der Test als bestanden.

Unser Testcode folgt dem Strukturmuster von Arrange-Act-Assert (AAA). Andere gängige Muster sind Given-When-Then und Setup-Exercise-Verify-Teardown (Teardown wird in der Regel nicht explizit für Komponententests benötigt), aber wir verwenden in diesem Artikel AAA.

Schauen wir uns an, wie unser Testbeispiel AAA folgt. Die erste Zeile, „arrange“, erstellt ein Person -Objekt, das getestet wird:

 Person person = new Person("Josh", "Hayden");

Die zweite Zeile, „act“, übt die Person.getDisplayName() Methode des Produktionscodes aus:

 String displayName = person.getDisplayName();

Die dritte Zeile, das „assert“, bestätigt, dass das Ergebnis wie erwartet ist.

 assertEquals("Hayden, Josh", displayName);

Intern verwendet der Aufruf von assertEquals() die equals-Methode des String-Objekts „Hayden, Josh“, um zu überprüfen, ob der tatsächliche Wert, der vom Produktionscode ( displayName ) zurückgegeben wird, übereinstimmt. Wenn es nicht übereinstimmte, wäre der Test als fehlgeschlagen markiert worden.

Beachten Sie, dass Tests oft mehr als eine Linie für jede dieser AAA-Phasen haben.

Unit-Tests und Produktionscode

Nachdem wir nun einige Testkonventionen behandelt haben, richten wir unsere Aufmerksamkeit darauf, den Produktionscode testbar zu machen.

Wir kehren zu unserer Person -Klasse zurück, in der ich eine Methode implementiert habe, um das Alter einer Person basierend auf ihrem Geburtsdatum zurückzugeben. Die Codebeispiele erfordern Java 8, um neue Daten und funktionale APIs nutzen zu können. So sieht die neue Person.java -Klasse aus:

Person.java
 // ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Das Ausführen dieses Kurses (zum Zeitpunkt des Schreibens) gibt bekannt, dass Joey 4 Jahre alt ist. Lassen Sie uns eine Testmethode hinzufügen:

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }

Es geht heute vorbei, aber was ist, wenn wir es in einem Jahr betreiben? Dieser Test ist nicht deterministisch und spröde, da das erwartete Ergebnis vom aktuellen Datum des Systems abhängt, auf dem der Test ausgeführt wird.

Stubbing und Injizieren eines Wertlieferanten

Wenn wir in der Produktion laufen, möchten wir das aktuelle Datum, LocalDate.now() , verwenden, um das Alter der Person zu berechnen, aber um einen deterministischen Test auch in einem Jahr von jetzt an durchzuführen, müssen Tests ihre eigenen currentDate Werte liefern.

Dies wird als Abhängigkeitsinjektion bezeichnet. Wir wollen nicht, dass unser Person -Objekt das aktuelle Datum selbst bestimmt, sondern diese Logik als Abhängigkeit übergeben. Komponententests verwenden einen bekannten, verkürzten Wert, und der Produktionscode ermöglicht, dass der tatsächliche Wert vom System zur Laufzeit bereitgestellt wird.

Lassen Sie uns einen LocalDate -Lieferanten zu Person.java :

Person.java
 // ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Um das Testen der Methode getAge() zu vereinfachen, haben wir sie so geändert, dass sie currentDateSupplier , einen LocalDate Lieferanten, zum Abrufen des aktuellen Datums verwendet. Wenn Sie nicht wissen, was ein Lieferant ist, empfehle ich, etwas über Lambda Built-in Functional Interfaces zu lesen.

Wir haben auch eine Abhängigkeitsinjektion hinzugefügt: Der neue Testkonstruktor ermöglicht es Tests, ihre eigenen aktuellen Datumswerte bereitzustellen. Der ursprüngliche Konstruktor ruft diesen neuen Konstruktor auf und übergibt eine statische Methodenreferenz von LocalDate::now , die ein LocalDate Objekt bereitstellt, sodass unsere Hauptmethode weiterhin wie zuvor funktioniert. Was ist mit unserer Testmethode? Lassen Sie uns PersonTest.java aktualisieren:

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }

Der Test fügt jetzt seinen eigenen Wert currentDate , sodass unser Test auch dann noch besteht, wenn er nächstes Jahr oder in einem beliebigen Jahr ausgeführt wird. Dies wird allgemein als Stubbing bezeichnet oder als Bereitstellung eines bekannten Werts, der zurückgegeben werden soll, aber wir mussten zuerst Person ändern, damit diese Abhängigkeit eingefügt werden kann.

Beachten Sie beim Erstellen des Person -Objekts die Lambda-Syntax ( ()->currentDate ). Dies wird wie vom neuen Konstruktor gefordert als Lieferant eines LocalDate behandelt.

Verspotten und Stubben eines Webdienstes

Wir sind bereit für unser Person -Objekt – dessen gesamte Existenz im JVM-Speicher war – um mit der Außenwelt zu kommunizieren. Wir möchten zwei Methoden hinzufügen: die Methode publishAge() , die das aktuelle Alter der Person veröffentlicht, und die Methode getThoseInCommon() , die Namen berühmter Personen zurückgibt, die denselben Geburtstag haben oder dasselbe Alter haben wie unsere Person . Angenommen, es gibt einen RESTful-Dienst namens „People Birthdays“, mit dem wir interagieren können. Wir haben dafür einen Java-Client, der aus der einzigen Klasse BirthdaysClient besteht.

com.example.birthdays.BirthdaysClient
 package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }

Lassen Sie uns unsere Person -Klasse erweitern. Wir beginnen damit, eine neue Testmethode für das gewünschte Verhalten von publishAge() . Warum mit dem Test beginnen und nicht mit der Funktionalität? Wir folgen den Prinzipien der testgetriebenen Entwicklung (auch bekannt als TDD), bei der wir zuerst den Test schreiben und dann den Code, um ihn zu bestehen.

PersonTest.java
 // … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }

An diesem Punkt kann der Testcode nicht kompiliert werden, weil wir die Methode publishAge() , die er aufruft, nicht erstellt haben. Sobald wir eine leere Person.publishAge() -Methode erstellt haben, ist alles vorbei. Wir sind jetzt bereit für den Test, um zu überprüfen, ob das Alter der Person tatsächlich im BirthdaysClient veröffentlicht wird.

Hinzufügen eines verspotteten Objekts

Da dies ein Komponententest ist, sollte er schnell und im Arbeitsspeicher ausgeführt werden, sodass der Test unser Person -Objekt mit einem Schein- BirthdaysClient -Client erstellt, sodass er nicht wirklich eine Webanforderung stellt. Der Test verwendet dann dieses Scheinobjekt, um zu überprüfen, ob es wie erwartet aufgerufen wurde. Dazu fügen wir eine Abhängigkeit vom Mockito-Framework (MIT-Lizenz) zum Erstellen von Mock-Objekten hinzu und erstellen dann ein mockiertes BirthdaysClient -Objekt:

PersonTest.java
 // ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }

Wir haben außerdem die Signatur des Person -Konstruktors erweitert, um ein BirthdaysClient -Objekt zu übernehmen, und den Test so geändert, dass er das verspottete BirthdaysClient -Objekt einfügt.

Hinzufügen einer Pseudo-Erwartung

Als Nächstes fügen wir am Ende unseres testPublishAge eine Erwartung hinzu, dass der BirthdaysClient aufgerufen wird. Person.publishAge() sollte es aufrufen, wie in unserem neuen PersonTest.java gezeigt:

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }

Unser Mockito-erweiterter BirthdaysClient verfolgt alle Aufrufe, die an seine Methoden vorgenommen wurden. Auf diese Weise überprüfen wir, dass keine Aufrufe an BirthdaysClient mit der Methode verifyZeroInteractions() sind, bevor publishAge() . Obwohl dies wohl nicht notwendig ist, stellen wir auf diese Weise sicher, dass der Konstruktor keine unerwünschten Aufrufe durchführt. In der verify() -Zeile geben wir an, wie der Aufruf von BirthdaysClient aussehen soll.

Beachten Sie, dass, da publishRegularPersonAge die IOException in seiner Signatur hat, wir sie auch zu unserer Testmethodensignatur hinzufügen.

An diesem Punkt schlägt der Test fehl:

 Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

Dies ist zu erwarten, da wir die erforderlichen Änderungen an Person.java noch nicht implementiert haben, da wir eine testgetriebene Entwicklung verfolgen. Wir werden diesen Test jetzt bestehen, indem wir die erforderlichen Änderungen vornehmen:

Person.java
 // ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }

Testen auf Ausnahmen

Wir haben den Produktionscode-Konstruktor dazu gebracht, einen neuen BirthdaysClient zu instanziieren, und publishAge() ruft jetzt den birthdaysClient auf. Alle Tests bestehen; alles ist grün. Toll! Beachten Sie jedoch, dass publishAge() die IOException verschluckt. Anstatt es herausblasen zu lassen, wollen wir es mit unserer eigenen PersonException in eine neue Datei namens PersonException.java :

PersonException.java
 package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

Wir implementieren dieses Szenario als neue Testmethode in PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }

Der Mockito-Aufruf doThrow() ruft stubs birthdaysClient auf, um eine Ausnahme auszulösen, wenn die Methode publishRegularPersonAge() aufgerufen wird. Wenn die PersonException nicht ausgelöst wird, bestehen wir den Test nicht. Andernfalls bestätigen wir, dass die Ausnahme ordnungsgemäß mit der IOException verkettet wurde, und überprüfen, ob die Ausnahmemeldung wie erwartet ist. Da wir im Moment keine Behandlung in unserem Produktionscode implementiert haben, schlägt unser Test fehl, da die erwartete Ausnahme nicht ausgelöst wurde. Folgendes müssen wir in Person.java ändern, damit der Test bestanden wird:

Person.java
 // ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }

Stubs: Wann und Behauptungen

Wir implementieren jetzt die Methode Person.getThoseInCommon() , sodass unsere Klasse Person.Java so aussieht.

Unser testGetThoseInCommon() überprüft im Gegensatz zu testPublishAge() nicht, ob bestimmte Aufrufe an birthdaysClient -Methoden erfolgt sind. Stattdessen verwendet es when -Aufrufe von Stub-Rückgabewerten für Aufrufe von findFamousNamesOfAge() und findFamousNamesBornOn() , die getThoseInCommon() machen muss. Wir behaupten dann, dass alle drei von uns bereitgestellten Kurznamen zurückgegeben werden.

Durch das Umschließen mehrerer Assertionen mit der JUnit 5-Methode assertAll() können alle Assertionen als Ganzes überprüft werden, anstatt nach der ersten fehlgeschlagenen Assertion anzuhalten. Wir fügen auch eine Nachricht mit assertTrue() ein, um bestimmte Namen zu identifizieren, die nicht enthalten sind. So sieht unsere Testmethode „Happy Path“ (ein ideales Szenario) aus (beachten Sie, dass dies von Natur aus kein robuster Satz von Tests ist, da es sich um einen „Happy Path“ handelt, aber wir werden später darüber sprechen, warum.

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }

Halten Sie den Testcode sauber

Obwohl oft übersehen, ist es ebenso wichtig, den Testcode frei von schwärender Duplizierung zu halten. Sauberer Code und Prinzipien wie „wiederhole dich nicht“ sind sehr wichtig für die Aufrechterhaltung einer qualitativ hochwertigen Codebasis, Produktions- und Testcode gleichermaßen. Beachten Sie, dass die neueste PersonTest.java nun einige Duplikate aufweist, da wir mehrere Testmethoden haben.

Um dies zu beheben, können wir eine Handvoll Dinge tun:

  • Extrahieren Sie das IOException-Objekt in ein privates letztes Feld.

  • Extrahieren Sie die Person -Objekterstellung in eine eigene Methode (in diesem Fall createJoeSixteenJan2() ), da die meisten Person-Objekte mit denselben Parametern erstellt werden.

  • Erstellen Sie eine assertCauseAndMessage() für die verschiedenen Tests, die ausgelöste PersonExceptions überprüfen.

Die Ergebnisse des sauberen Codes sind in dieser Wiedergabe der Datei PersonTest.java zu sehen.

Testen Sie mehr als den Happy Path

Was sollen wir tun, wenn ein Person -Objekt ein Geburtsdatum hat, das nach dem aktuellen Datum liegt? Fehler in Anwendungen sind oft auf unerwartete Eingaben oder mangelnde Voraussicht in Eck-, Kanten- oder Grenzfällen zurückzuführen. Es ist wichtig, diese Situationen so gut wie möglich zu antizipieren, und Einheitentests sind häufig ein geeigneter Ort, um dies zu tun. Beim Erstellen unserer Person und PersonTest haben wir einige Tests für erwartete Ausnahmen aufgenommen, aber es war keineswegs vollständig. Beispielsweise verwenden wir LocalDate , das keine Zeitzonendaten darstellt oder speichert. Unsere Aufrufe von LocalDate.now() geben jedoch ein LocalDate basierend auf der Standardzeitzone des Systems zurück, die einen Tag früher oder später als die des Benutzers eines Systems sein kann. Diese Faktoren sollten mit geeigneten Tests und implementierten Verhaltensweisen berücksichtigt werden.

Auch Grenzen sollten geprüft werden. Stellen Sie sich ein Person -Objekt mit einer getDaysUntilBirthday() Methode vor. Zu den Tests sollte gehören, ob der Geburtstag der Person im laufenden Jahr bereits vergangen ist, ob der Geburtstag der Person heute ist und wie sich ein Schaltjahr auf die Anzahl der Tage auswirkt. Diese Szenarien können abgedeckt werden, indem ein Tag vor dem Geburtstag der Person, der Tag und ein Tag nach dem Geburtstag der Person, an dem das nächste Jahr ein Schaltjahr ist, überprüft wird. Hier ist der zugehörige Testcode:

PersonTest.java
 // ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }

Integrationstests

Wir haben uns hauptsächlich auf Unit-Tests konzentriert, aber JUnit kann auch für Integrations-, Akzeptanz-, Funktions- und Systemtests verwendet werden. Solche Tests erfordern oft mehr Einrichtungscode, z. B. das Starten von Servern, das Laden von Datenbanken mit bekannten Daten usw. Während wir oft Tausende von Einheitentests in Sekunden ausführen können, kann die Ausführung großer Integrationstestsuiten Minuten oder sogar Stunden dauern. Integrationstests sollten im Allgemeinen nicht verwendet werden, um zu versuchen, jede Permutation oder jeden Pfad durch den Code abzudecken; Unit-Tests sind dafür besser geeignet.

Das Erstellen von Tests für Webanwendungen, die Webbrowser dazu bringen, Formulare auszufüllen, auf Schaltflächen zu klicken, auf das Laden von Inhalten zu warten usw., erfolgt üblicherweise mit Selenium WebDriver (Apache 2.0-Lizenz) in Verbindung mit dem „Page Object Pattern“ (siehe das SeleniumHQ-Github-Wiki und Martin Fowlers Artikel über Seitenobjekte).

JUnit ist effektiv zum Testen von RESTful-APIs mit der Verwendung eines HTTP-Clients wie Apache HTTP Client oder Spring Rest Template (HowToDoInJava.com bietet ein gutes Beispiel).

In unserem Fall mit dem Person -Objekt könnte ein Integrationstest die Verwendung des echten BirthdaysClient anstelle eines Scheinclients beinhalten, mit einer Konfiguration, die die Basis-URL des People Birthdays-Dienstes angibt. Ein Integrationstest würde dann eine Testinstanz eines solchen Dienstes verwenden, überprüfen, ob die Geburtstage darin veröffentlicht wurden, und berühmte Personen im Dienst erstellen, die zurückgegeben würden.

Andere JUnit-Funktionen

JUnit hat viele zusätzliche Funktionen, die wir in den Beispielen noch nicht untersucht haben. Wir werden einige beschreiben und Referenzen für andere bereitstellen.

Prüfvorrichtungen

Es sollte beachtet werden, dass JUnit eine neue Instanz der @Test zum Ausführen jeder @Test-Methode erstellt. JUnit bietet auch Anmerkungs-Hooks, um bestimmte Methoden vor oder nach allen oder jeder der @Test Methoden auszuführen. Diese Hooks werden häufig zum Einrichten oder Bereinigen von Datenbank- oder Scheinobjekten verwendet und unterscheiden sich zwischen JUnit 4 und 5.

JUnit 4 JUnit 5 Für eine statische Methode?
@BeforeClass @BeforeAll Jawohl
@AfterClass @AfterAll Jawohl
@Before @BeforeEach Nein
@After @AfterEach Nein

In unserem PersonTest Beispiel haben wir uns dafür entschieden, das Mock-Objekt BirthdaysClient in den @Test Methoden selbst zu konfigurieren, aber manchmal müssen komplexere Mock-Strukturen mit mehreren Objekten erstellt werden. @BeforeEach (in JUnit 5) und @Before (in JUnit 4) sind dafür oft geeignet.

Die @After* -Anmerkungen sind häufiger bei Integrationstests als bei Unit-Tests, da die JVM-Garbage-Collection die meisten für Unit-Tests erstellten Objekte verarbeitet. Die Annotationen @BeforeClass und @BeforeAll werden am häufigsten für Integrationstests verwendet, bei denen kostspielige Setup- und Teardown-Aktionen einmal und nicht für jede Testmethode durchgeführt werden müssen.

Für JUnit 4 lesen Sie bitte den Leitfaden für Testvorrichtungen (die allgemeinen Konzepte gelten weiterhin für JUnit 5).

Testsuiten

Manchmal möchten Sie mehrere verwandte Tests ausführen, aber nicht alle Tests. In diesem Fall können Gruppierungen von Tests zu Testsuiten zusammengestellt werden. Informationen dazu, wie Sie dies in JUnit 5 tun, finden Sie im JUnit 5-Artikel von HowToProgram.xyz und in der Dokumentation des JUnit-Teams für JUnit 4.

@Nested und @DisplayName von JUnit 5

JUnit 5 fügt die Fähigkeit hinzu, nicht statische verschachtelte innere Klassen zu verwenden, um die Beziehung zwischen Tests besser darzustellen. Dies sollte denjenigen sehr vertraut sein, die mit verschachtelten Beschreibungen in Test-Frameworks wie Jasmine für JavaScript gearbeitet haben. Die inneren Klassen sind mit @Nested annotiert, um dies zu verwenden.

Die Annotation @DisplayName ist ebenfalls neu in JUnit 5 und ermöglicht es Ihnen, den Test für die Berichterstellung im Zeichenfolgenformat zu beschreiben, der zusätzlich zur Testmethoden-ID angezeigt wird.

Obwohl @Nested und @DisplayName unabhängig voneinander verwendet werden können, können sie zusammen für eindeutigere Testergebnisse sorgen, die das Verhalten des Systems beschreiben.

Hamcrest Matcher

Obwohl das Hamcrest-Framework selbst nicht Teil der JUnit-Codebasis ist, bietet es eine Alternative zur Verwendung traditioneller Assert-Methoden in Tests und ermöglicht einen aussagekräftigeren und lesbareren Testcode. Sehen Sie sich die folgende Überprüfung an, die sowohl ein herkömmliches assertEquals als auch ein Hamcrest-asserthat verwendet:

 //Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest kann sowohl mit JUnit 4 als auch mit 5 verwendet werden. Das Tutorial von Vogella.com zu Hamcrest ist ziemlich umfassend.

Zusätzliche Ressourcen

  • Der Artikel Unit Tests, How to Write Testable Code and Why it Matters behandelt spezifischere Beispiele für das Schreiben von sauberem, testbarem Code.

  • Build with Confidence: A Guide to JUnit Tests untersucht verschiedene Ansätze für Komponenten- und Integrationstests und warum es am besten ist, einen auszuwählen und dabei zu bleiben

  • Das JUnit 4 Wiki und das JUnit 5 Benutzerhandbuch sind immer ein ausgezeichneter Bezugspunkt.

  • Die Mockito-Dokumentation enthält Informationen zu zusätzlichen Funktionen und Beispielen.

JUnit ist der Weg zur Automatisierung

Wir haben viele Aspekte des Testens in der Java-Welt mit JUnit untersucht. Wir haben uns Einheiten- und Integrationstests mit dem JUnit-Framework für Java-Codebasen angesehen, JUnit in Entwicklungs- und Build-Umgebungen integriert, die Verwendung von Mocks und Stubs mit Lieferanten und Mockito, gängige Konventionen und bewährte Code-Praktiken, was zu testen ist und einiges davon andere großartige JUnit-Funktionen.

Jetzt ist der Leser an der Reihe, in der geschickten Anwendung, Wartung und Nutzung der Vorteile automatisierter Tests mit dem JUnit-Framework zu wachsen.