Cómo probar excepciones en Java con JUnit

1. Introducción

En este tutorial rápido, vamos a explorar cómo probar si se lanza una excepción y cómo comprobar si no se lanza ninguna excepción utilizando la biblioteca JUnit. Cubriremos tanto las versiones de JUnit 4 como JUnit 5, lo que proporciona un marco completo para el manejo de excepciones en pruebas unitarias.

Además, aprenderemos cómo utilizar métodos específicos para validar lanzamientos de excepciones y cómo proceder si queremos asegurarnos de que un bloque de código se ejecute sin errores. Esto es especialmente útil para programadores que desean garantizar la robustez de su código en aplicaciones Java.

¿Por qué es importante probar excepciones?

En el desarrollo de software, un manejo adecuado de las excepciones es crucial. Permite que nuestro programa funcione de manera confiable, incluso bajo condiciones inesperadas. Las pruebas automatizadas son una forma efectiva de asegurar que las excepciones se manejen adecuadamente. Esto nos ayuda a prevenir errores en la producción y a mejorar la calidad del código.

2. JUnit 5

Comencemos nuestra discusión sobre el manejo de excepciones en JUnit 5.

2.1. Afirmar que se lanza una excepción

JUnit 5 presenta el método assertThrows en su API de aserciones Jupiter para verificar excepciones. Este método toma el tipo de la excepción esperada y una interfaz funcional Executable, donde podemos pasar el código bajo prueba a través de una expresión lambda.

Aquí hay un ejemplo:

@Test
void whenExceptionThrown_thenAssertionSucceeds() {
    Exception exception = assertThrows(NumberFormatException.class, () -> {
        Integer.parseInt("1a");
    });

    String expectedMessage = "For input string";
    String actualMessage = exception.getMessage();
    
    assertTrue(actualMessage.contains(expectedMessage));
}

Si se lanza la excepción esperada, assertThrows devuelve la excepción, lo que nos permite también comprobar el mensaje de error. Es importante notar que esta afirmación se satisface cuando el código lanzado arroja una excepción del tipo NumberFormatException o de cualquier tipo derivado.

Podemos hacer que la prueba pase si cambiamos el tipo de excepción esperada a RuntimeException:

@Test
void whenDerivedExceptionThrown_thenAssertionSucceeds() {
    Exception exception = assertThrows(RuntimeException.class, () -> {
        Integer.parseInt("1a");
    });

    String expectedMessage = "For input string";
    String actualMessage = exception.getMessage();
    
    assertTrue(actualMessage.contains(expectedMessage));
}

El método assertThrows() permite un control más preciso sobre la lógica de afirmación de excepciones, ya que podemos utilizarlo en partes específicas del código.

2.2. Afirmar que no se lanza ninguna excepción

A veces es fundamental asegurarse de que un bloque de código no arroje ninguna excepción. JUnit 5 proporciona una manera fácil de realizar esta verificación:

@Test
void givenABlock_whenExecutes_thenEnsureNoExceptionThrown() {
    assertDoesNotThrow(() -> {
        Integer.parseInt("100");
    });
}

El método assertDoesNotThrow() ejecuta el bloque de código proporcionado. Si no se lanza una excepción, la prueba pasa. Si se lanza una excepción, la prueba falla.

2.3. Afirmar que no se lanza un tipo específico de excepción

En algunas ocasiones, podemos necesitar afirmar que el código no causa un tipo particular de excepción. JUnit no proporciona un método integrado para ello; sin embargo, podemos crear un método personalizado para manejar este escenario.

Primero, créemos una interfaz funcional que nos permita definir genéricamente una interfaz que se pueda utilizar en nuestra implementación personalizada:

@FunctionalInterface
public interface Executable {
    void execute() throws Exception;
}

Ahora podemos usar esto en la implementación personalizada para pasar la clase de excepción deseada y ejecutar el bloque de código:

private  void assertSpecificExceptionIsNotThrown(Class exceptionClass, Executable executable) {
    try {
        executable.execute();
    } catch (Exception e) {
        if (exceptionClass.isInstance(e)) {
            fail(e.getClass().getSimpleName() + " was thrown");
        } else {
            // Cualquier otro tipo de excepciones se ignoran y la prueba pasa
            LOG.info("Caught exception: " + e.getClass().getName() + ", but ignoring since it is not an instance of " + exceptionClass.getName());
        }
    }
}

Podemos utilizar este método de la siguiente manera:

@Test
void givenASpecificExceptionType_whenBlockExecutes_thenEnsureThatExceptionIsNotThrown() {
    assertSpecificExceptionIsNotThrown(IllegalArgumentException.class, () -> {
        int i = 100 / 0;
    });
}

Esta prueba falla si el bloque de código lanza un IllegalArgumentException o cualquiera de sus subclases. La prueba pasa para cualquier otro tipo de excepción o si no se genera ninguna excepción. Esto es útil en escenarios donde necesitamos asegurarnos de que un tipo particular de excepción nunca se arroje.

3. JUnit 4

Pasemos ahora a JUnit 4 para ver cómo manejamos las afirmaciones de excepciones en esta versión.

3.1. Afirmar que se lanza una excepción

En JUnit 4, podemos simplemente usar el atributo expected de la anotación @Test para declarar que esperamos que se lance una excepción en cualquier lugar del método de prueba anotado.

Por ejemplo:

@Test(expected = NullPointerException.class)
public void whenExceptionThrown_thenExpectationSatisfied() {
    String test = null;
    test.length();
}

Aquí hemos declarado que esperamos que nuestro código de prueba resulte en un NullPointerException.

Sin embargo, si necesitamos verificar algunas otras propiedades de la excepción, podemos usar la regla ExpectedException.

@Rule
public ExpectedException exceptionRule = ExpectedException.none();

@Test
public void whenExceptionThrown_thenRuleIsApplied() {
    exceptionRule.expect(NumberFormatException.class);
    exceptionRule.expectMessage("For input string");
    Integer.parseInt("1a");
}

En este ejemplo, primero declaramos la regla ExpectedException. Luego, en nuestra prueba, estamos afirmando que el código que intenta analizar un valor de Integer resultará en un NumberFormatException con el mensaje “For input string”.

3.2. Afirmar que no se lanza ninguna excepción

A diferencia de JUnit 5, JUnit 4 no proporciona un método integrado para afirmar que no se generan excepciones del código. Sin embargo, podemos implementar esta lógica fácilmente:

private void assertNoExceptionIsThrown(Executable executable) {
    try {
        executable.execute();
    } catch (Exception e) {
        fail(e.getClass().getSimpleName() + " was thrown");
    }
}

Podemos utilizar la interfaz Executable definida anteriormente para pasar el bloque de código. Si se lanza alguna excepción, la capturamos y fallamos explícitamente la prueba:

@Test
public void givenABlock_whenExecuted_thenEnsureThatNoExceptionAreThrown() {
    assertNoExceptionIsThrown(() -> {
        int d = 100 / 10;
    });
}

Esta prueba fallará si se lanza cualquier excepción en el bloque de código.

4. Conclusión

En este artículo, cubrimos cómo afirmar excepciones tanto con JUnit 4 como con JUnit 5. Examinamos métodos para afirmar que se lanza una excepción, así como para asegurar que no se lancen excepciones. Además, creamos una implementación personalizada para manejar tipos específicos de excepciones.

Saber cómo manejar excepciones y probar su comportamiento es vital para cualquier programador de Java. Implementar correctamente estas pruebas no solo asegura que nuestras aplicaciones son robustas y resistentes, sino que también mejora la mantenibilidad a largo plazo.

Consejos prácticos para programadores de Java:

  • Siempre prueba los caminos de error en tu código, no solo los caminos felices.
  • Utiliza assertThrows en JUnit 5 para manejar excepciones de manera eficiente.
  • Verifica tanto el tipo de excepción como el mensaje de error cuando sea necesario.
  • Considera escribir métodos de utilidad personalizados para validar excepciones si necesitas lógica más compleja.
  • No descuides las pruebas que aseguran que no se lanza una excepción, especialmente en métodos críticos de negocio.

El manejo correcto de las excepciones en tus pruebas unitarias puede marcar la diferencia entre un código confiable y uno lleno de errores. ¡Feliz programación!