Suggerimenti per i test di unità con AssertJ

Unit testing è diventato uno standard nello sviluppo. Molti strumenti possono essere utilizzati in vari modi. Questo articolo dimostra un paio di suggerimenti o, diciamo, best practice che hanno funzionato bene per me.

In questo articolo imparerai

Non abusare dei controlli NPE

Tendiamo tutti ad evitare NullPointerException il più possibile nel codice principale perché può causare conseguenze spiacevoli. Credo che la nostra preoccupazione principale non sia evitare NPE nei test. Il nostro obiettivo è verificare il comportamento del componente testato in modo chiaro, leggibile e affidabile.

Cattiva Pratica

Molte volte in passato ho utilizzato l’asserzione isNotNull anche quando non era necessaria, come nell’esempio seguente:

Java

 

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

Questo test produce errori di questo tipo:

Plain Text

 

java.lang.AssertionError: 
Expecting actual not to be null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

Buona Pratica

Anche se l’asserzione aggiuntiva isNotNull non è davvero dannosa, dovrebbe essere evitata per i seguenti motivi:

  • Non aggiunge alcun valore aggiuntivo. È solo più codice da leggere e mantenere.
  • Il test fallisce comunque quando service è null e vediamo la vera causa radice dell’errore. Il test continua a svolgere il suo scopo.
  • Il messaggio di errore prodotto è persino migliore con l’asserzione di AssertJ.

Vedi l’asserzione del test modificata qui sotto.

Java

 

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

Il test modificato produce un errore simile a questo:

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)

Nota: L’esempio può essere trovato in SimpleSpringTest.

Confrontare i Valori e non il Risultato

Di tanto in tanto, scriviamo un test corretto, ma in modo “sbagliato”. Significa che il test funziona esattamente come previsto e verifica il nostro componente, ma l’errore non fornisce abbastanza informazioni. Pertanto, il nostro obiettivo è asserire il valore e non il risultato della comparazione.

Cattiva Pratica

Vediamo un paio di tali cattivi test:

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();

Alcuni errori dei test precedenti sono mostrati di seguito.

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)

Buona Pratica

La soluzione è abbastanza semplice con AssertJ e la sua API fluente. Tutti i casi menzionati sopra possono essere facilmente riscritti come:

Java

 

// #1
assertThat(argument).contains("o");

// #2
assertThat(result).isInstanceOf(String.class);

// #3
assertThat("").isBlank();

// #4
assertThat(testMethod).isPresent();

Gli stessi errori menzionati prima ora forniscono più valore.

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)

Nota: L’esempio può essere trovato in SimpleParamTests.

Raccogliere Asserzioni Correlate

La concatenazione delle asserzioni e l’indentazione del codice correlato aiutano molto nella chiarezza e leggibilità dei test.

Cattiva Pratica

Nel scrivere un test, potremmo finire con un test corretto, ma meno leggibile. Immaginiamo un test in cui vogliamo trovare paesi e fare queste verifiche:

  1. Conta i paesi trovati. 
  2. Asserisci il primo elemento con diversi valori.

Tali test possono apparire come questo esempio:

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");
}

Buona Pratica

Anche se il test precedente è corretto, dovremmo migliorare molto la leggibilità raggruppando asserzioni correlate insieme (righe 9-11). L’obiettivo qui è asserire risultato una volta e scrivere molte asserzioni concatenate come necessario. Vedi la versione modificata qui sotto.

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");
		});
}

Nota: L’esempio può essere trovato in CountryRepositoryOtherTests.

Prevenire Test di Successo Positivo Falso

Quando si utilizza un metodo di asserzione con l’argomento ThrowingConsumer, l’argomento deve contenere anche assertThat nel consumer. Altrimenti, il test passerebbe sempre, anche quando la comparazione fallisce, il che significa un test sbagliato. Il test fallisce solo quando un’asserzione lancia un’eccezione RuntimeException o AssertionError. Credo sia chiaro, ma è facile dimenticarsene e scrivere un test sbagliato. Mi capita di tanto in tanto.

Pratica Sbagliata

Immaginiamo di avere un gruppo di codici di paese e di voler verificare che ogni codice soddisfi una certa condizione. Nel nostro caso fittizio, vogliamo asserire che ogni codice di paese contenga il carattere “a”. Come puoi vedere, è assurdo: abbiamo codici in maiuscolo, ma non applichiamo la sensibilità al caso nell’asserzione.

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> countryCode.contains("a"));
}

Sorprendentemente, il nostro test è passato con successo.

Pratica Corretta

Come accennato all’inizio di questa sezione, il nostro test può essere corretto facilmente aggiungendo assertThat nel consumer (linea 7). Il test corretto dovrebbe essere simile a questo:

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}

Ora il test fallisce come previsto, con il messaggio di errore corretto.

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)

Catena di Asserzioni

L’ultimo suggerimento non è tanto una pratica, quanto piuttosto una raccomandazione. L’API fluente di AssertJ dovrebbe essere utilizzata per creare test più leggibili.

Asserzioni Non In Catena

Consideriamo il test listLogs, il cui scopo è testare la registrazione di un componente. L’obiettivo qui è verificare:

  • Numero dichiarato di log raccolti
  • Assicurarsi dell’esistenza di DEBUG e INFO messaggi di log
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" );
		});
}

Concatenazione delle Asserzioni

Utilizzando l’API fluida menzionata e la concatenazione, possiamo modificare il test in questo modo:

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" );
		});
}

Nota: l’esempio può essere trovato in AppleTest.

Riepilogo e Codice Sorgente

Il framework AssertJ fornisce molto aiuto con la loro API fluida. In questo articolo, sono stati presentati diversi suggerimenti e consigli al fine di produrre test più chiari e affidabili. Si prega di tenere presente che la maggior parte di questi suggerimenti sono soggettivi. Dipende dalle preferenze personali e dal codice dello stile.

Il codice utilizzato può essere trovato nei miei repository:

Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj