Eenheidstests zijn uitgegroeid tot een standaard onderdeel van ontwikkeling. Er zijn veel tools die op verschillende manieren voor gebruikt kunnen worden. Dit artikel demonstreert een paar tips of, laten we zeggen, beste praktijken die voor mij goed werken.
In dit artikel leer je
- Hoe je schoon en leesbaar unit tests schrijft met JUnit en Assert-frameworks
- Hoe te voorkomen dat er valse positieven optreden bij bepaalde tests
- Wat te vermijden bij het schrijven van unit tests
Geen overdreven gebruik maken van NPE-controles
We proberen allemaal NullPointerException
in de hoofdcode zoveel mogelijk te vermijden, omdat het tot lelijke gevolgen kan leiden. Ik geloof dat ons belangrijkste doel niet is om NPE in tests te vermijden. Ons doel is om het gedrag van een geteste component op een schoon, leesbaar en betrouwbaar manier te verifiëren.
Slechte Praktijk
In het verleden heb ik vaak de isNotNull
assert gebruikt, zelfs als het niet nodig was, zoals in het voorbeeld hieronder:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Deze test produceert fouten zoals deze:
java.lang.AssertionError:
Expecting actual not to be null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
Goede Praktijk
Hoewel de extra isNotNull
assert niet echt schadelijk is, zou het moeten worden vermeden vanwege de volgende redenen:
- Het voegt geen extra waarde toe. Het is gewoon meer code om te lezen en te onderhouden.
- De test faalt sowieso wanneer
service
null
is en we zien de echte oorzaak van de mislukking. De test vervult nog steeds zijn doel. - Het geproduceerde foutbericht is zelfs beter met de AssertJ-assertie.
Zie de aangepaste testassertie hieronder.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
De aangepaste test produceert een fout zoals deze:
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)
Opmerking: Het voorbeeld is te vinden in SimpleSpringTest.
Assert Values en Niet het Resultaat
Af en toe schrijven we een correcte test, maar op een “slechte” manier. Het betekent dat de test precies zo werkt als bedoeld en onze component verifieert, maar de mislukking biedt niet genoeg informatie. Daarom is ons doel om de waarde te asserten en niet het vergelijkingsresultaat.
Slechte Praktijk
Laten we een paar van dergelijke slechte tests bekijken:
// #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();
Sommige fouten uit de tests hierboven worden hieronder weergegeven.
#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)
Goede Praktijk
De oplossing is vrij eenvoudig met AssertJ en zijn fluent API. Al de hierboven genoemde gevallen kunnen gemakkelijk herschreven worden als:
// #1
assertThat(argument).contains("o");
// #2
assertThat(result).isInstanceOf(String.class);
// #3
assertThat("").isBlank();
// #4
assertThat(testMethod).isPresent();
Dezelfde fouten als eerder genoemd leveren nu meer waarde op.
#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)
Opmerking: Het voorbeeld kan worden gevonden in SimpleParamTests.
Groepeer Gerelateerde Asserties Bij Elkaar
Het koppelen van asserties en een gerelateerde code-indeling helpen enorm bij de helderheid en leesbaarheid van de test.
Slechte Gewoonte
Als we een test schrijven, kunnen we uitkomen op een correcte, maar minder leesbare test. Stel je een test voor waarin we landen willen vinden en deze controles willen uitvoeren:
- Tel de gevonden landen.
- Asserteer de eerste entry met verschillende waarden.
Zulke tests kunnen er zo uitzien als dit voorbeeld:
@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");
}
Goede Gewoonte
Hoewel de vorige test correct is, moeten we de leesbaarheid aanzienlijk verbeteren door de gerelateerde asserties bij elkaar te groeperen (regels 9-11). Het doel hier is result
eenmaal te asserteren en zo veel mogelijk gekoppelde asserties te schrijven als nodig is. Zie de aangepaste versie hieronder.
@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");
});
}
Opmerking: Het voorbeeld kan worden gevonden in CountryRepositoryOtherTests.
Voorkom Fout-Positief Succesvolle Test
Wanneer een verklaring-methode wordt gebruikt met het ThrowingConsumer
argument, moet dit argument ook de assertThat
bevatten in de consument. Anders zou de test altijd slagen – zelfs wanneer de vergelijking mislukt, wat betekent dat de test verkeerd is. De test faalt alleen wanneer een verklaring een RuntimeException
of AssertionError
uitzondering werpt. Ik denk dat het duidelijk is, maar het is gemakkelijk te vergeten en een verkeerde test te schrijven. Het gebeurt wel eens met mij.
Slechte Praktijk
Stel dat we een aantal landcodes hebben en we willen controleren of elke code aan een bepaalde voorwaarde voldoet. In ons voorbeeld willen we verklaren dat elke landcode de karakter “a” bevat. Zoals je kunt zien, is het onzinnig: we hebben codes in hoofdletters, maar passen we geen hoofdlettergevoeligheid toe in de verklaring.
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> countryCode.contains("a"));
}
Verrassend genoeg slaagde onze test met succes.
Goede Praktijk
Zoals eerder vermeld in dit gedeelte, kan onze test gemakkelijk worden gecorrigeerd met een extra assertThat
in de consument (regel 7). De juiste test zou er zo uit moeten zien:
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}
Nu faalt de test zoals verwacht met het juiste foutbericht.
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)
Keten Verklaringen
Het laatste advies is niet echt een praktijk, maar eerder een aanbeveling. De AssertJ vloeiende API moet worden gebruikt om leesbare tests te creëren.
Niet-Keten Verklaringen
Laten we de listLogs
test beschouwen, waarvan het doel is om de logboekregistratie van een component te testen. Het doel hier is om te controleren:
- Aangegeven aantal verzamelde logs
- Beweer het bestaan van
DEBUG
enINFO
logbericht
@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" );
});
}
Kettingvergelijkingen
Met de genoemde vloeiende API en de ketting kunnen we de test op deze manier wijzigen:
@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" );
});
}
Opmerking: het voorbeeld kan worden gevonden in AppleTest.
Samenvatting en broncode
Het AssertJ-framework biedt veel hulp met hun vloeiende API. In dit artikel werden verschillende tips en aanbevelingen gepresenteerd om duidelijkere en betrouwbarere tests te produceren. Let op dat de meeste van deze aanbevelingen subjectief zijn. Het hangt af van persoonlijke voorkeuren en code-stijl.
De gebruikte broncode kan worden gevonden in mijn repositories:
Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj