<div class="page"> <div class="cover text-center"> <img class="mx-auto" src=/itb/images/logo_mislata.png alt="logo"> # Tests unitaris amb JUnit <div class="text-end fit-content ms-auto my-3 mt-auto pt-3"> <p><strong>Autor:</strong> Joan Puigcerver Ibáñez</p> <p><strong>Correu electrònic:</strong> j.puigcerveribanez@edu.gva.es</p> <p><strong>Curs:</strong> 2024/2025</p> </div> <div> <p class="fw-bold mb-0">Llicència: BY-NC-SA</p> <p class="d-none d-md-block">(Reconeixement - No Comercial - Compartir Igual)</p> <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ca" target="_blank"> <img class="mx-auto" src="/itb/images/license.png" alt="Licence"/> </a> </div><!--license--> </div><!--cover--> </div><!--page--> {:toc} ## Introducció El __test unitari__ és una tècnica de verificació de programari que s'utilitza per assegurar que cada part ("unitat") del programa funciona correctament. Aquesta tècnica es basa en la idea de dividir el programa en parts més petites i escriure tests per a cada part per comprovar que totes elles funcionen correctament. Això és útil perquè, quan es produeixen canvis en un programa, és més fàcil determinar què ha anat malament si es poden comprovar les parts individuals del programa. Això també pot ajudar a detectar errors més ràpidament i a prevenir la introducció de nous errors durant el manteniment del programa. ## Tests unitaris en Java Java té diverses eines per escriure i executar tests unitaris. Una de les més comunes és __JUnit__, que és una llibreria de Java que proporciona un marc per escriure i executar tests unitaris. Per utilitzar JUnit, primer hauràs de descarregar-lo i afegir-lo al teu projecte Java. ### Instal·lar JUnit mitjançant Maven - __Documentació IntelliJ__: https://www.jetbrains.com/help/idea/junit.html Podem instal·lar la llibreria JUnit mitjançant __Maven__. __Maven__ és una eina de gestió de projectes que es basa en un conjunt de normes que defineixen com s'han de construir i empaquetar els projectes, així com les dependències amb altres projectes o llibreries. Això fa que siga molt més senzill de gestionar projectes a gran escala i ho fa molt més reproducible, ja que es pot utilitzar Maven per crear fàcilment una estructura de directoris comuna i automatitzar tasques com la compilació, testeig i empaquetat. Quan creem un projecte en IntelliJ, ens pregunta si volem utilitzar Maven. En el nostre cas, ja vam crear el projecte com un projecte Maven. Per poder utilitzar JUnit al nostre projecte, hem d'afegir la llibreria com una dependència: 1. Obri el fitxer __pom.xml__ que es troba a l'arrel del teu projecte. 2. Una vegada obert, prem `Alt+Insert` i selecciona __Add dependency__. ![](/itb/DAM-ED/UD5/img/pom_xml.png) 3. A la finestra, escriu `org.junit.jupiter:junit-jupiter` a la barra de búsqueda. Sel·lecciona la dependència necessària i prem __Add__ per afegir-la. ![](/itb/DAM-ED/UD5/img/junit_dependency.png) Aquesta acció harià afegit al teu __pom.xml__ un text semblant a: ```xml <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> </dependency> </dependencies> ``` 4. Ara hem d'aplicar els canvis realitzars a l'script de Maven. Premeu `Ctrl+Maj+O` o feu clic a __Load Maven Changes__ a la notificació que apareix a l'extrem superior dret de l'editor. ![](/itb/DAM-ED/UD5/img/pom_xml_updated.png) Després de actualitzar, hauria de quedar aixina: ![](/itb/DAM-ED/UD5/img/pom_xml_after.png) ### Crear tests Anem a crear tests per comprovar que el programa [DebugRockPaperScissors](../../UD1/exercicis/03_debugging.html#exercici-2%3A-debugrockpaperscissors) funciona correctament. Per poder realitzar els tests, s'ha creat una nova versió del programa que incorpora el mètode `int guanya(String jugador1, String jugador2)`, que determina quin jugador ha guanyat la partida: ```java package ud5.examples; import java.util.Locale; import java.util.Scanner; public class DebugRockPaperScissors { /** * Determina quin jugador ha guanyat la partida de pedra paper i tisores. * <p> * Aquest mètode retorna 0 si hi ha hagut un empat, 1 si el jugador1 ha guanyat la partida, * i 2 si el jugador2 ha guanyat. * * @param jugador1 Jugada del jugador1 (pedra, paper o tisores) * @param jugador2 Jugada del jugador1 (pedra, paper o tisores) * @return 0 si hi ha un empat; 1 si guanya jugador1; 2 si guanya jugador 2. */ public static int guanya(String jugador1, String jugador2){ if(jugador1.equals(jugador2)) return 0; else if (jugador1.equals("paper")) { return jugador2.equals("pedra") ? 1 : 2; } else if (jugador1.equals("pedra")) { return jugador2.equals("paper") ? 1 : 2; } else { return jugador2.equals("paper") ? 1 : 2; } } public static void main(String[] args) { Scanner in = new Scanner(System.in).useLocale(Locale.US); System.out.print("Introdueix l'elecció del jugador 2 (pedra/paper/tisores): "); String p1 = in.nextLine(); System.out.print("Introdueix l'elecció del jugador 2 (pedra/paper/tisores): "); String p2 = in.nextLine(); int guanyador = guanya(p1, p2); if (guanyador == 0) System.out.println("Empat"); else System.out.printf("Guanya el jugador %d\n", guanyador); } } ``` Anem a comprovar si el mètode està ben implementat utilitzant tests unitaris: 1. Situat sobre el nom de la classe `DebugRockPaperScissors` i prem `Alt+Insert`. Selecciona __Test__. També pots prémer `Alt+Enter` o clicar el botó dret i selecciona __Show Context Actions__. Des del menú, selecciona __Create Test__. ![](/itb/DAM-ED/UD5/img/test_context_menu.png) 2. Selecciona els mètodes que vols provar en el diàleg i prem __OK__. ![](/itb/DAM-ED/UD5/img/test_dialog.png) 3. L'acció anterior ha creat un nou fixer en `src/test/java`, la carpeta dessignada on guardar els tests del projecte. El nou fitxer es troba en la mateixa estructura de paquests que el codi font: `ud5.examples`. ![](/itb/DAM-ED/UD5/img/test_created.png) 4. Anem a crear un test per comprovar si mètode determina correctament si la pedra guanya a les tisores. Per fer-ho utilitzarem, `assertEquals()` que comprovarà que els parametres d'entrada i eixida son iguals. Podem utilitzar `@DisplayName` per donar-li un nom més descriptiu al test: ```java @Test @DisplayName("Guanya: Pedra vs Tisores") void pedraGuanyaTisores(){ assertEquals(1, DebugRockPaperScissors.guanya("pedra", "tisores")); } ``` 5. Comprovem també si hi ha un empat quan els dos jugadors elegeixen 'tisores'. ```java @Test @DisplayName("Empat: Tisores vs Tisores") void tisoresEmpataTisores(){ assertEquals(0, DebugRockPaperScissors.guanya("tisores", "tisores")); } ``` ### Llançar els tests Una vegada s'han establit els tests, anem a executar-los per veure si el mètode `int guanya(String jugador1, String jugador2)` funciona correctanent. Per executar-los, s'ha de clicar el botó ![](/itb/DAM-ED/UD1/img/debugger/run.svg){.icon} __Run__. ![](/itb/DAM-ED/UD5/img/test_run.png) Una vegada executats, podem consultar els resultats: ![](/itb/DAM-ED/UD5/img/test_results.png) Es poden observals els resultats de cada test per separat. En aquest cas tenim: - El test __Empat: Tisores vs Tisores__ s'ha executant correctament. - El test __Guanya: Pedra vs Tisores__ ha fallat. S'esperava (__expected__) que guanyara el jugador 1 i s'ha obtingut (__actual__) que ha guanyat el jugador 2. ## Assert En JUnit, la instrucció __assert__ és una instrucció que s'utilitza per comprovar que una determinada condició és certa durant l'execució d'un test unitari. Si la condició no es compleix, el test llançarà una excepció (error) del tipus `AssertionError`. En JUnit, hi ha molts tipus diferents d'instruccions "assert" disponibles per fer verificacions diferents durant l'execució de testos unitaris. ### assertEquals La instrucció `assertEquals` s'utilitza per comprovar que dos valor són iguals, on __expected__ és el valor que esperaves i __actual__ és el valor que has obtingut. Si "expected" i "actual" són iguals, el test continuarà executant-se. Si no ho són, el test fallarà amb una `AssertionError`. Aquest mètode utilitza el mètode `equals` dels objectes per determinar si els objetes són iguals o no. ```java assertEquals(10, 3 + 7), // Integer assertEquals('b', 'a' + 1), // Char assertEquals(1.5, 3.0 / 2), // Double assertEquals(1.5, 3.1 / 2, 0.1), // Double with tolerance (delta) assertEquals("HELLO", "hello".toUpperCase()) // String ``` En el cas de dels tipus `double` i `float`, es pot definir una tolerància (`delta`): ```java assertEquals(1.5, 3.1 / 2, 0.1), // Double with tolerance (delta) ``` ### assertTrue i assertFalse La instrucció `assertTrue(expression)` i `assertFalse(expression)` s'utilitza per comprovar que una expressió es vertadera o falsa, respectivament. Si no ho és, el test fallarà. ```java assertTrue(isPositive(5)); assertFalse(isPositive(-1)); ``` ### assertNull i assertNotNull La instrucció `assertNull(obj)` i `assertNotNull(obj)` s'utilitza per comprovar si un objecte és null o no, respectivament. Si no té el valor demanat, el test fallarà. ```java String a = null; String b = "Joan"; assertNull(a); assertNotNull(b); ``` ### assertSame i assertNotSame La instrucció `assertSame(obj1, obj2)` i `assertNotSame(obj1, obj2)` s'utilitza per comprovar si dos objectes son el mateix o no, respectivament. Si no compleixen la condició, el test fallarà. Aquest mètode utilitza l'operador `==`, que en els objectes mira si exactament són el mateix objecte, és a dir, que la referència en mèmoria es la mateixa. ```java String a = new String("Joan"); String b = new String("Joan"); assertSame(a, a); assertNotSame(a, b); ``` ::: note Utilitzem new String per forçar el compilador que cree dos objectes diferents en dos referències de memòria diferents. Vegeu: https://stackoverflow.com/questions/15805578/will-two-strings-with-same-content-be-stored-in-the-same-memory-location ::: ### assertArrayEquals La instrucció `assertArrayEquals(expected, actual)` s'utilitza per comprovar si dos arrays són iguals. Si no ho son, el test fallarà. `assertArrayEquals` pot ser utilitzada en arrays de diferents tipus de dades, que poden ser tipus de dades __primitives__ o objectes. ```java double[] intExpected = {1, 2, 3}; double[] intActual = {1, 2, 3}; assertArrayEquals(intExpected, intActual), double[] doubleExpected = {1.0, 2.0, 3.0}; double[] doubleActual = {1.0, 2.0, 3.0}; assertArrayEquals(doubleExpected, doubleActual ), String[] stringExpected = {"Hello", "World!"}; String[] stringActual = {"Hello", "World!"}; assertArrayEquals(stringExpected, stringActual) ``` En el cas de arrays de tipus `double[]`, es pot definir una tolerància (`delta`): ```java double[] doubleToleranceExpected = {1.0, 2.0, 3.0}; double[] doubleToleranceActual = {1.09, 2.0, 2.91}; assertArrayEquals(doubleToleranceExpected, doubleToleranceActual, 0.1), ``` ### assertAll La instrucció `assertAll(Executable... executables)` s'utilitza per comprovar que tots els `assert` proporcionats no llancen un error del tipus `AssertionError`. En altres paraules, serveix per comprovar que totes les comprovacions en un cas de prova es compleixen. Aquesta instrucció és útil, ja que encara que un cas de prova dóne error, tots els següents casos de prova també s'executaran. ```java assertFalse(1 > 0); // Aquest cas de prova fallarà, i per tant, assertTrue(1 > 0); // la següent intrucció no s'executarà ``` En canvi: ```java assertAll( () -> assertFalse(1 > 0), // Aquest cas de prova fallarà, però () -> assertTrue(1 > 0) // la següent intrucció també es comprovarà. ) ``` Aquesta instrucció és útil quan un únic cas de prova ha de realitzar més d'una comprovació. ```java @Test @DisplayName("Multiply possitive numbers") void multiplyPositiveNumbers(){ assertAll( () -> assertEquals(1, multiply(1, 1)), () -> assertEquals(2, multiply(1, 2)), () -> assertEquals(4, multiply(2, 2)), () -> assertEquals(6, multiply(2, 3)), () -> assertEquals(100, multiply(10, 10)) ); } ``` La sintaxi `() ->` es correspon a [Lambda expressions](https://www.w3schools.com/java/java_lambda.asp). ## Example assertions Aquest fitxer anirà a la carpeta test: __src/test/java/ud5/examples__, - <a href="/itb/DAM-ED/files/ud5/examples/AssertionTest.java" download="AssertionTest.java">AssertionTest.java</a> ```java package ud5.examples; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class AssertionTest { @Test @DisplayName("assertEquals") void assertMultipleEquals(){ assertAll( () -> assertEquals(10, 3 + 7), // Integer () -> assertEquals('b', 'a' + 1), // Char () -> assertEquals(1.5, 3.0 / 2), // Double () -> assertEquals(1.5, 3.1 / 2, 0.1), // Double with tolerance (delta) () -> assertEquals("HELLO", "hello".toUpperCase()) // String ); } @Test @DisplayName("assertTrue & assertFalse") void assertMultipleTrueFalse(){ assertAll( () -> assertTrue(-10 < 0), () -> assertFalse(-10 > 0) ); } @Test @DisplayName("assertNull & assertNotNull") void assertMultipleNull(){ String a = null; String b = "Joan"; assertAll( () -> assertNull(a), () -> assertNotNull(b) ); } @Test @DisplayName("assertSame & assertNotSame") void assertMultipleSame(){ // https://stackoverflow.com/questions/15805578/will-two-strings-with-same-content-be-stored-in-the-same-memory-location String a = new String("Joan"); String b = new String("Joan"); assertAll( () -> assertSame(a, a), () -> assertNotSame(a, b) ); } @Test @DisplayName("assertArrayEquals") void assertMultipleArray(){ double[] intExpected = {1, 2, 3}; double[] intActual = {1, 2, 3}; double[] doubleExpected = {1.0, 2.0, 3.0}; double[] doubleActual = {1.0, 2.0, 3.0}; double[] doubleToleranceExpected = {1.0, 2.0, 3.0}; double[] doubleToleranceActual = {1.09, 2.0, 2.91}; String[] stringExpected = {"Hello", "World!"}; String[] stringActual = {"Hello", "World!"}; assertAll( () -> assertArrayEquals(intExpected, intActual), () -> assertArrayEquals(doubleExpected, doubleActual ), () -> assertArrayEquals(doubleToleranceExpected, doubleToleranceActual, 0.1), () -> assertArrayEquals(stringExpected, stringActual) ); } } ```