Unit testing ist zu einem Standardteil der Entwicklung geworden. Es stehen viele Tools zur Verfügung, die auf verschiedene Weisen genutzt werden können. Dieser Artikel demonstriert einige Tipps oder, sagen wir, Best Practices, die für mich gut funktioniert haben.
In diesem Artikel lernen Sie
- Wie man saubere und lesbare Einheitstests mit JUnit und Assert-Frameworks schreibt
- Wie man falsche positive Tests in einigen Fällen vermeidet
- Was zu vermeiden ist, wenn man Einheitstests schreibt
Nicht übermäßig NPE-Checks verwenden
Wir alle neigen dazu, NullPointerException
in der Hauptcodebasis so weit wie möglich zu vermeiden, da sie zu hässlichen Folgen führen kann. Ich glaube, unser Hauptinteresse liegt nicht darin, NPE in Tests zu vermeiden. Unser Ziel ist es, das Verhalten eines getesteten Komponenten auf eine saubere, lesbare und zuverlässige Weise zu überprüfen.
Schlechte Praxis
In der Vergangenheit habe ich oft isNotNull
-Assertion verwendet, auch wenn es nicht erforderlich war, wie im folgenden Beispiel:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Dieser Test produziert Fehler wie diesen:
java.lang.AssertionError:
Expecting actual not to be null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
Gute Praxis
Obwohl die zusätzliche isNotNull
-Assertion nicht wirklich schädlich ist, sollte sie aus den folgenden Gründen vermieden werden:
- Sie bringt keinen zusätzlichen Wert. Es ist einfach mehr Code zum Lesen und Wartung.
- Der Test scheitert sowieso, wenn
service
null
ist und wir den eigentlichen Fehlerursache erkennen. Der Test erfüllt nach wie vor seine Zwecke. - Der generierte Fehlermeldung ist sogar besser mit der AssertJ-Assertion.
Sehen Sie die modifizierte Testassertion unten.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Der modifizierte Test erzeugt einen Fehler wie folgt:
java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
Hinweis: Das Beispiel finden Sie in SimpleSpringTest.
Werte und nicht das Ergebnis beurteilen
Ab und zu schreiben wir einen korrekten Test, aber auf eine „schlechte“ Weise. Das bedeutet, der Test funktioniert genau wie beabsichtigt und überprüft unser Komponente, aber der Fehler liefert nicht genügend Informationen. Daher ist unser Ziel, den Wert und nicht das Vergleichsergebnis zu beurteilen.
Schlechte Praxis
Sehen wir uns einige solcher schlechten Tests an:
// #1
assertThat(argument.contains("o")).isTrue();
// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();
// #3
assertThat("".isBlank()).isTrue();
// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();
Einige Fehler aus den Tests oben sind unten dargestellt.
#1
Expecting value to be true but was false
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
#3
Expecting value to be true but was false
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)
Gute Praxis
Die Lösung ist mit AssertJ und seiner fließenden API ziemlich einfach. Alle oben genannten Fälle können leicht umgeschrieben werden:
// #1
assertThat(argument).contains("o");
// #2
assertThat(result).isInstanceOf(String.class);
// #3
assertThat("").isBlank();
// #4
assertThat(testMethod).isPresent();
Die genannten Fehler liefern jetzt mehr Wert.
#1
Expecting actual:
"Hello"
to contain:
"f"
at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
#3
Expecting blank but was: "a"
at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)
Hinweis: Das Beispiel finden Sie in SimpleParamTests.
Zusammenhängende Assertions gruppieren
Die Verkettung von Assertions und eine entsprechende Code-Einrückung tragen erheblich zur Klarheit und Lesbarkeit der Tests bei.
Schlechte Praxis
Wenn wir einen Test schreiben, können wir zu einem korrekten, aber weniger lesbaren Test gelangen. Stellen wir uns einen Test vor, bei dem wir Länder suchen und diese Prüfungen durchführen möchten:
- Zähle die gefundenen Länder.
- Assertiere den ersten Eintrag mit mehreren Werten.
Solche Tests können beispielsweise so aussehen:
@Test
void listCountries() {
List<Country> result = ...;
assertThat(result).hasSize(5);
var country = result.get(0);
assertThat(country.getName()).isEqualTo("Spain");
assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}
Gute Praxis
Obwohl der vorherige Test korrekt ist, sollten wir die Lesbarkeit durch Gruppieren der zusammengehörigen Assertions (Zeilen 9-11) stark verbessern. Das Ziel hierbei ist es, result
einmal zu assertieren und so viele verkettete Assertions wie nötig zu schreiben. Sehen Sie sich die modifizierte Version unten an.
@Test
void listCountries() {
List<Country> result = ...;
assertThat(result)
.hasSize(5)
.singleElement()
.satisfies(c -> {
assertThat(c.getName()).isEqualTo("Spain");
assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
});
}
Hinweis: Das Beispiel finden Sie in CountryRepositoryOtherTests.
Falschen positiven Test erfolgreich verhindern
Wenn eine Assertionsmethode mit dem ThrowingConsumer
Argument verwendet wird, muss das Argument auch assertThat
im Consumer enthalten. Andernfalls würde der Test immer bestehen – selbst wenn die Vergleiche fehlschlagen, was bedeutet, dass der Test falsch ist. Der Test schlägt nur fehl, wenn eine Assertion eine RuntimeException
oder AssertionError
Ausnahme auslöst. Ich vermute, es ist klar, aber es ist leicht zu vergessen und den falschen Test zu schreiben. Das passiert mir ab und zu.
Schlechte Praxis
Stellen wir uns vor, wir haben einige Länderkürzel und möchten überprüfen, ob jedes Kürzel eine bestimmte Bedingung erfüllt. In unserem Beispiel möchten wir sicherstellen, dass jedes Länderkürzel den Buchstaben „a“ enthält. Wie Sie sehen, ist das Unsinn: Wir haben Kürzel in Großbuchstaben, aber wir wenden im Vergleich keine Groß- und Kleinschreibung an.
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> countryCode.contains("a"));
}
Überraschenderweise ist unser Test erfolgreich bestanden.
Gute Praxis
Wie zu Beginn dieses Abschnitts erwähnt, kann unser Test leicht korrigiert werden, indem zusätzlich assertThat
im Consumer verwendet wird (Zeile 7). Der korrekte Test sollte folgendermaßen aussehen:
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}
Jetzt schlägt der Test wie erwartet mit der richtigen Fehlermeldung fehl.
java.lang.AssertionError:
Expecting all elements of:
["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:
"CZ"
error:
Expecting actual:
"CZ"
to contain:
"a"
(ignoring case)
at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)
Ketten von Assertions
Der letzte Hinweis ist eher eine Empfehlung als eine Praxis. Die fließende API von AssertJ sollte genutzt werden, um lesbarere Tests zu erstellen.
Nicht-Ketten von Assertions
Betrachten wir den listLogs
Test, dessen Zweck ist, die Protokollierung eines Komponenten zu testen. Das Ziel hier ist zu überprüfen:
- Festgestellte Anzahl der gesammelten Logs
- Feststellung der Existenz von
DEBUG
undINFO
Log-Nachrichten
@Test
void listLogs() throws Exception {
ListAppender<ILoggingEvent> logAppender = ...;
assertThat( logAppender.list ).hasSize(2);
assertThat( logAppender.list ).anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
});
assertThat( logAppender.list ).anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(INFO);
assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
});
}
Ketten von Assertions
Mit der erwähnten fließenden API und der Verkettung können wir den Test so ändern:
@Test
void listLogs() throws Exception {
ListAppender<ILoggingEvent> logAppender = ...;
assertThat( logAppender.list )
.hasSize(2)
.anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
})
.anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(INFO);
assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
});
}
Hinweis: das Beispiel finden Sie in AppleTest.
Zusammenfassung und Quellcode
Das AssertJ-Framework bietet viel Unterstützung mit ihrer fließenden API. In diesem Artikel wurden verschiedene Tipps und Hinweise präsentiert, um klarere und zuverlässigere Tests zu erstellen. Bitte beachten Sie, dass die meisten dieser Empfehlungen subjektiv sind. Es hängt von persönlichen Vorlieben und dem Code-Stil ab.
Der verwendete Quellcode findet sich in meinen Repositories:
Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj