Tips voor Unit Testen met AssertJ

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

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:

Java

 

@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

Deze test produceert fouten zoals deze:

Plain Text

 

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.

Java

 

@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

De aangepaste test produceert een fout zoals deze:

Java

 

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:

Java

 

// #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.

Plain Text

 

#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:

Java

 

// #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.

Plain Text

 

#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:

  1. Tel de gevonden landen. 
  2. Asserteer de eerste entry met verschillende waarden.

Zulke tests kunnen er zo uitzien als dit voorbeeld:

Java

 

@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.

Java

 

@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.

Java

 

@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:

Java

 

@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.

Plain Text

 

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 en INFO logbericht
Java

 

@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:

Java

 

@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