Tutorial su JUnit 5, parte 1: test di unità con JUnit 5, Mockito e Hamcrest

JUnit 5 è il nuovo standard de facto per lo sviluppo di unit test in Java. Questa versione più recente ha lasciato i vincoli di Java 5 e ha integrato molte funzionalità di Java 8, in particolare il supporto per le espressioni lambda.

In questa prima metà di un'introduzione in due parti a JUnit 5, inizierai con i test con JUnit 5. Ti mostrerò come configurare un progetto Maven per usare JUnit 5, come scrivere test usando le annotazioni @Teste @ParameterizedTest, e come lavorare con le nuove annotazioni del ciclo di vita in JUnit 5. Vedrai anche un breve esempio di utilizzo dei tag di filtro e ti mostrerò come integrare JUnit 5 con una libreria di asserzioni di terze parti, in questo caso Hamcrest . Infine, riceverai una rapida introduzione tutorial all'integrazione di JUnit 5 con Mockito, in modo da poter scrivere test unitari più robusti per sistemi complessi e reali.

scarica Ottieni il codice Ottieni il codice sorgente per gli esempi in questo tutorial. Creato da Steven Haines per JavaWorld.

Sviluppo basato su test

Se hai sviluppato codice Java per un certo periodo di tempo, probabilmente hai familiarità con lo sviluppo basato sui test, quindi manterrò questa sezione breve. Tuttavia, è importante capire perché scriviamo unit test, nonché le strategie che gli sviluppatori impiegano durante la progettazione di unit test.

Lo sviluppo guidato dai test (TDD) è un processo di sviluppo software che intreccia codifica, test e progettazione. È un approccio test-first che mira a migliorare la qualità delle tue applicazioni. Lo sviluppo basato sui test è definito dal seguente ciclo di vita:

  1. Aggiungi un test.
  2. Esegui tutti i tuoi test e osserva il nuovo test che fallisce.
  3. Implementa il codice.
  4. Esegui tutti i tuoi test e osserva la riuscita del nuovo test.
  5. Refactoring del codice.

La Figura 1 mostra questo ciclo di vita TDD.

Steven Haines

C'è un duplice scopo nello scrivere test prima di scrivere il codice. In primo luogo, ti costringe a pensare al problema aziendale che stai cercando di risolvere. Ad esempio, come dovrebbero comportarsi gli scenari di successo? Quali condizioni dovrebbero fallire? Come dovrebbero fallire? In secondo luogo, testare prima ti dà più fiducia nei tuoi test. Ogni volta che scrivo test dopo aver scritto il codice, devo sempre romperli per assicurarmi che stiano effettivamente rilevando errori. Scrivere i test prima evita questo passaggio aggiuntivo.

Scrivere test per il percorso felice di solito è facile: dato un buon input, la classe dovrebbe restituire una risposta deterministica. Ma scrivere casi di test negativi (o falliti), specialmente per componenti complessi, può essere più complicato.

Ad esempio, prendi in considerazione la scrittura di test per un repository di database. Sul percorso felice, inseriamo un record nel database e riceviamo indietro l'oggetto creato, comprese le chiavi generate. In realtà, dobbiamo anche considerare la possibilità di un conflitto, come l'inserimento di un record con un valore di colonna univoco già detenuto da un altro record. Inoltre, cosa succede quando il repository non riesce a connettersi al database, forse perché il nome utente o la password sono cambiati? Cosa succede se si verifica un errore di rete in transito? Cosa succede se la richiesta non viene completata entro il limite di timeout definito?

Per creare un componente robusto, è necessario considerare tutti gli scenari probabili e improbabili, sviluppare test per essi e scrivere il codice per soddisfare tali test. Più avanti nell'articolo, esamineremo le strategie per creare diversi scenari di errore, insieme ad alcune delle nuove funzionalità di JUnit 5 che possono aiutarti a testare questi scenari.

Adozione di JUnit 5

Se usi JUnit da un po 'di tempo, alcune delle modifiche in JUnit 5 saranno una regolazione. Ecco un riepilogo di alto livello delle differenze tra le due versioni:

  • JUnit 5 è ora impacchettato nel org.junit.jupitergruppo, il che cambia il modo in cui lo includerai nei tuoi progetti Maven e Gradle.
  • JUnit 4 richiedeva un JDK minimo di JDK 5; JUnit 5 richiede un minimo di JDK 8.
  • JUnit 4 di @Before, @BeforeClass, @After, e @AfterClassannotazioni sono stati sostituiti da @BeforeEach, @BeforeAll, @AfterEache @AfterAll, rispettivamente.
  • L' @Ignoreannotazione di JUnit 4 è stata sostituita @Disableddall'annotazione.
  • L' @Categoryannotazione è stata sostituita @Tagdall'annotazione.
  • JUnit 5 aggiunge un nuovo set di metodi di asserzione.
  • I corridori sono stati sostituiti con estensioni, con una nuova API per gli implementatori di estensioni.
  • JUnit 5 introduce ipotesi che interrompono l'esecuzione di un test.
  • JUnit 5 supporta classi di test nidificate e dinamiche.

Esploreremo la maggior parte di queste nuove funzionalità in questo articolo.

Test unitario con JUnit 5

Cominciamo in modo semplice, con un esempio end-to-end di configurazione di un progetto per utilizzare JUnit 5 per uno unit test. Il listato 1 mostra una MathToolsclasse il cui metodo converte un numeratore e un denominatore in un file double.

Listato 1. Un esempio di progetto JUnit 5 (MathTools.java)

 package com.javaworld.geekcap.math; public class MathTools { public static double convertToDecimal(int numerator, int denominator) { if (denominator == 0) { throw new IllegalArgumentException("Denominator must not be 0"); } return (double)numerator / (double)denominator; } }

Abbiamo due scenari principali per testare la MathToolsclasse e il suo metodo:

  • Un test valido , in cui passiamo numeri interi diversi da zero per numeratore e denominatore.
  • Uno scenario di fallimento , in cui passiamo un valore zero per il denominatore.

Il Listato 2 mostra una classe di test JUnit 5 per testare questi due scenari.

Listato 2. Una classe di test JUnit 5 (MathToolsTest.java)

 package com.javaworld.geekcap.math; import java.lang.IllegalArgumentException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class MathToolsTest { @Test void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.75, result); } @Test void testConvertToDecimalInvalidDenominator() { Assertions.assertThrows(IllegalArgumentException.class, () -> MathTools.convertToDecimal(3, 0)); } }

In Listing 2, the testConvertToDecimalInvalidDenominator method executes the MathTools::convertToDecimal method inside an assertThrows call. The first argument is the expected type of exception to be thrown. The second argument is a function that will throw that exception. The assertThrows method executes the function and validates that the expected type of exception is thrown.

The Assertions class and its methods

The org.junit.jupiter.api.Test annotation denotes a test method. Note that the @Test annotation now comes from the JUnit 5 Jupiter API package instead of JUnit 4's org.junit package. The testConvertToDecimalSuccess method first executes the MathTools::convertToDecimal method with a numerator of 3 and a denominator of 4, then asserts that the result is equal to 0.75. The org.junit.jupiter.api.Assertions class provides a set of static methods for comparing actual and expected results. The Assertions class has the following methods, which cover most of the primitive data types:

  • assertArrayEquals compares the contents of an actual array to an expected array.
  • assertEquals compares an actual value to an expected value.
  • assertNotEquals compares two values to validate that they are not equal.
  • assertTrue validates that the provided value is true.
  • assertFalse validates that the provided value is false.
  • assertLinesMatch compares two lists of Strings.
  • assertNull validates that the provided value is null.
  • assertNotNull validates that the provided value is not null.
  • assertSame validates that two values reference the same object.
  • assertNotSame validates that two values do not reference the same object.
  • assertThrows validates that the execution of a method throws an expected exception (you can see this in the testConvertToDecimalInvalidDenominator example above).
  • assertTimeout validates that a supplied function completes within a specified timeout.
  • assertTimeoutPreemptively validates that a supplied function completes within a specified timeout, but once the timeout is reached it kills the function's execution.

If any of these assertion methods fail, the unit test is marked as failed. That failure notice will be written to the screen when you run the test, then saved in a report file.

Using delta with assertEquals

When using float and double values in an assertEquals, you can also specify a delta that represents a threshold of difference between the two. In our example we could have added a delta of 0.001, in case 0.75 was actually returned as 0.750001.

Analyzing your test results

In addition to validating a value or behavior, the assert methods can also accept a textual description of the error, which can help you diagnose failures. For example:

 Assertions.assertEquals(0.75, result, "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); Assertions.assertEquals(0.75, result, () -> "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); 

The output will show the expected value of 0.75 and the actual value. It will also display the specified message, which can help you understand the context of the error. The difference between the two variations is that the first one always creates the message, even if it is not displayed, whereas the second one only constructs the message if the assertion fails. In this case, the construction of the message is trivial, so it doesn't really matter. Still, there is no need to construct an error message for a test that passes, so it's usually a best practice to use the second style.

Finally, if you're using an IDE like IntelliJ to run your tests, each test method will be displayed by its method name. This is fine if your method names are readable, but you can also add a @DisplayName annotation to your test methods to better identify the tests:

@Test @DisplayName("Test successful decimal conversion") void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.751, result); }

Running your unit test

In order to run JUnit 5 tests from a Maven project, you need to include the maven-surefire-plugin in the Maven pom.xml file and add a new dependency. Listing 3 shows the pom.xml file for this project.

Listing 3. Maven pom.xml for an example JUnit 5 project

  4.0.0 com.javaworld.geekcap junit5 jar 1.0-SNAPSHOT    org.apache.maven.plugins maven-compiler-plugin 3.8.1  8 8    org.apache.maven.plugins maven-surefire-plugin 3.0.0-M4    junit5 //maven.apache.org   org.junit.jupiter junit-jupiter 5.6.0 test   

JUnit 5 dependencies

JUnit 5 packages its components in the org.junit.jupiter group and we need to add the junit-jupiter artifact, which is an aggregator artifact that imports the following dependencies:

  • junit-jupiter-api defines the API for writing tests and extensions.
  • junit-jupiter-engine è l'implementazione del motore di test che esegue gli unit test.
  • junit-jupiter-params fornisce supporto per test parametrizzati.

Successivamente, dobbiamo aggiungere il maven-surefire-pluginplug-in di build per eseguire i test.

Infine, assicurati di includere il maven-compiler-plugincon una versione di Java 8 o successiva, in modo da poter utilizzare le funzionalità di Java 8 come lambda.

Eseguirlo!

Usa il seguente comando per eseguire la classe di test dal tuo IDE o da Maven:

mvn clean test

Se hai successo, dovresti vedere un output simile al seguente:

 [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.javaworld.geekcap.math.MathToolsTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.04 s - in com.javaworld.geekcap.math.MathToolsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.832 s [INFO] Finished at: 2020-02-16T08:21:15-05:00 [INFO] ------------------------------------------------------------------------