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
- Come scrivere unit test chiari e leggibili utilizzando JUnit e framework di asserzioni
- Come evitare test falsamente positivi in alcuni casi
- Cosa evitare quando si scrivono unit test
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:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Questo test produce errori di questo tipo:
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.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Il test modificato produce un errore simile a questo:
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:
// #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.
#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:
// #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.
#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:
- Conta i paesi trovati.
- Asserisci il primo elemento con diversi valori.
Tali test possono apparire come questo esempio:
@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.
@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.
@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:
@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.
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
eINFO
messaggi di log
@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:
@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