Pruebas Unitarias con JUnit 5

Creando pruebas unitarias con Junit 5

En este Post estaremos conversando de las pruebas unitarias en el desarrollo de software, para que son utilizadas que ventajas y características tienen y cómo podemos implementarlas con JUnit 5 en nuestras aplicaciones desarrolladas en Java.Con las pruebas unitarias podemos hacer validaciones a pequeñas unidades de nuestro código como lo son las funciones o métodos, sin embargo, esto no nos va a asegurar al 100% que nuestro código está libre de errores, estas son mucho más robustas cuando se acompañan con otros tipos de pruebas como lo son la de integración y de rendimiento. Poor otro lado el ejemplo que vimos nos da una idea de cómo podemos generar nuestras pruebas unitarias y como ejecutarlas, sin embargo, este ejemplo es sumamente básico y no abarca todo lo que podemos hacer con JUnit, recomendamos que sigan leyendo sobre las funcionalidades que este framework proporciona en su documentación oficial o en otras fuentes que aborden a profundidad esta herramienta.

Entorno

  • Java 20

  • Maven 3.9.1

  • Junit 5

Que son las Pruebas Unitarias

Son una forma efectiva de comprobar el correcto funcionamiento de las unidades individuales más pequeñas. Sirven para asegurar que cada unidad funcione correcta y eficientemente por separado. Se caracterizan por proporcionar una garantía de calidad del sistema.

Características

  • Automatizable: No debería requerir una intervención manual. Esto es especialmente útil para integración continua.

  • Completas: Deben cubrir la mayor cantidad de código.

  • Rápidas: Deben poder ejecutarse en fracciones de segundo, caso contrario serán obviadas.

  • Repetibles o Reutilizables: No se deben crear pruebas que sólo puedan ser ejecutadas una sola vez. También es útil para integración continua.

  • Independientes: La ejecución de una prueba no debe afectar a la ejecución de otra.

  • Profesionales: Las pruebas deben ser consideradas igual que el código, con la misma profesionalidad, documentación, entre otros.

Ventajas

  • Fomentan el cambio: facilitan cambiar el código para mejorar su estructura​, puesto que permiten asegurar de que los nuevos cambios no han introducido defectos.

  • Simplifica la integración: Permiten llegar a la fase de integración con un grado alto de seguridad de que el código está funcionando correctamente.

  • Documenta el código: Las propias pruebas son documentación del código, puesto que ahí se puede ver cómo utilizarlo.

  • Separación de la interfaz y la implementación: Dado que la única interacción entre los casos de prueba y las unidades bajo prueba son las interfaces de estas últimas, se puede cambiar cualquiera de los dos sin afectar al otro. (mock object - maqueta) que habilitan de forma aislada (unitaria) el comportamiento de objetos complejos.

  • Los errores están más acotados y son más fáciles de localizar: Dado que tenemos pruebas unitarias que pueden desenmascararlos.

Limitaciones

  • No descubrirán todos los errores del código.

  • No descubrirán errores de integración, problemas de rendimiento y otros problemas que afectan a todo el sistema en su conjunto.

  • Son más efectivas si se usan en conjunto con otras pruebas de software (integración, performance, entre otras).

JUnit

Framework utilizado para la programación de pruebas unitarias en aplicaciones Java, permitiendo realizar la ejecución de clases de manera controlada, para evaluar si el funcionamiento de cada uno de los métodos de la clase se comporta como se espera.

En la página ofiicial puede encontrar muchas más información sobre este poderoso framework.

Prueba Unitaria con Junit

¡Imaginemos que tenemos una clase Main en la cual tenemos una función sayHello que nos devuelve un String con un mensaje de saludo Hello world!, para comprobar que esta función tenga el comportamiento esperado podemos usar una prueba unitaria que nos permita validar este comportamiento.

Clase Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println(sayHello());
    }

    public static String sayHello(){
        return "Hello world!";
    }
}

Para crear nuestra prueba unitaria debemoos de ir al paquete de pruebas, en la mayoria de los casos este esta en src/main/test/java, sin embargo, esto dependera de la estructura que hayamos definido en nuestro proyecto.

En este paquete crearemos una clase llamada MainTest.java, es en esta clase donde generaremos nuestras pruebas unitarias.

Creamos el metodo sayHelloTest que no tenga retorno la cual analizara el comportamiento de nuestra funcion .

void sayHelloTest()

Luego tenemos que indicarle a JUnit que este metodo es una prueba unitaria, esto lo hacemos usando con la anotación @Test. Puede encontrar mayor detalle de esta anotación en aqui.

En este punto sabiendo cual es el texto esperado podemos implementar en el método sayHelloTest el comportamiento, es decir, cuando ejecute la función sayHello de la clase Main debo de obtener uns String con el valor Hello world!,

Algo como esto:

// Given
String expectedMessage = "Hello world!";

// When
String message = Main.sayHello();

Ahora, como validamos el resultado?

Para esto tenemos los Assertions, JUnit cuenta con un gran numero de afirmaciones, puede encontrar mayor detalle de estos aqui

En este punto sabiendo cual es el texto esperado podemos implementar en el método Hello world! por lo que con Assertions.assertEquals podemos validar este resultado.

// Then
Assertions.assertEquals(expectedMessage, message);

Al final nuestra clase MainTest debe verse algo siimilar a esto

Maintest.java

class MainTest {

    @Test
    void sayHello() {
        // Given
        String expectedMessage = "Hello world!";

        // When
        String message = Main.sayHello();

        // Then
        Assertions.assertEquals(expectedMessage, message);
    }
}

Para ejecutar nuestro test vamos a hacer uso del goal de Maven test el cual busca los tests que tengamos declarados en nuestro proyecto y los ejecuta, por ejemplo: mvn test, con este comando si todo anda bien veremos un resultados similar al siguiente:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.hvs.lab.MainTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.106 s - in com.hvs.lab.MainTest
[INFO] 
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for common-exercises 1.0.0:
[INFO]
[INFO] common-exercises ................................... SUCCESS [  0.046 s]
[INFO] unit-tests ......................................... SUCCESS [  3.522 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.112 s
[INFO] Finished at: 2023-06-23T19:50:25-05:00
[INFO] ------------------------------------------------------------------------

Como ven la prueba se ejecuto de forma satisfactoria Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Ahora imaginemos que nuestro método no tiene el comportamiento esperado, cambiemos intencionalmente el mensaje de salida por otro, por ejemplo Hola Mundo!

Si volvemos a ejecutar nuestra prueba ahora veremos algo similar a esto:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.hvs.lab.MainTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.112 s <<< FAILURE! - in com.hvs.lab.MainTest
[ERROR] com.hvs.lab.MainTest.sayHello  Time elapsed: 0.064 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <Hello world!> but was: <Hola Mundo!>

Como pueden ver la prueba detecto el problema y no da un mensaje indicándonos que no es el resultado esperado, incluso nos nuestra una breve descripción de que fue lo que paso expected: <Hello world!> but was: <Hola Mundo!>, esperaba un mensaje, pero reciibiio otro.

Conclusiones

Con las pruebas unitarias podemos hacer validaciones a pequeñas unidades de nuestro código como lo son las funciones o métodos, sin embargo, esto no nos va a asegurar al 100% que nuestro código está libre de errores, estas son mucho más robustas cuando se acompañan con otros tipos de pruebas como lo son la de integración y de rendimiento. Poor otro lado el ejemplo que vimos nos da una idea de cómo podemos generar nuestras pruebas unitarias y como ejecutarlas, sin embargo, este ejemplo es sumamente básico y no abarca todo lo que podemos hacer con JUnit, recomendamos que sigan leyendo sobre las funcionalidades que este framework proporciona en su documentación oficial o en otras fuentes que aborden a profundidad esta herramienta.

Referencias