Introducción: «¿Por qué mi prueba falla cuando debería pasar?»
\n\n¿Alguna vez has escrito una prueba unitaria que debía capturar una excepción, pero en su lugar fallaba con un error rojo? ¿Te suena familiar? La pregunta «How do you assert that a certain exception is thrown in JUnit tests?» en Stack Overflow ha acumulado miles de visitas y cientos de comentarios. Y no es casualidad: probar excepciones es uno de esos temas donde incluso los desarrolladores experimentados a veces tropiezan.
\n\nEn 2025, cuando la calidad del código se ha convertido en un factor crítico (especialmente en microservicios y pipelines de IA), saber cómo verificar correctamente las excepciones no es solo una «buena práctica», sino una competencia básica. En este artículo, analizaremos todas las formas actuales de afirmar excepciones en JUnit 5, mostraremos los obstáculos y proporcionaremos una lista de verificación de mejores prácticas. ¡Allá vamos!
\n\n¿Por qué es importante probar las excepciones?
\n\nImagina: estás escribiendo el método divide(int a, int b). Si b == 0, el método debe lanzar ArithmeticException. Sin una prueba para este comportamiento, corres el riesgo de que dentro de un mes otro desarrollador (o tú mismo) elimine accidentalmente la verificación y la aplicación falle en producción con un error incomprensible.
Las pruebas de excepciones son tu seguro. Documentan el comportamiento esperado del sistema en casos límite. Y JUnit 5 ofrece varias herramientas elegantes para ello.
\n\nMétodo 1: assertThrows — el rey de las pruebas modernas
\n\nA partir de JUnit 5 (y su paquete org.junit.jupiter.api), el método assertThrows se ha convertido en el estándar de facto. ¿Por qué? Porque es conciso, legible y funcional.
import static org.junit.jupiter.api.Assertions.assertThrows;\n\n@Test\nvoid whenDivideByZero_thenArithmeticException() {\n assertThrows(ArithmeticException.class, \n () -> calculator.divide(10, 0));\n}\n\n\n¿Qué sucede aquí? assertThrows recibe la clase de la excepción esperada y una lambda con el código que debe lanzarla. Si se lanza la excepción, la prueba es verde. Si no, es roja. Simple y claro.
Variante avanzada: verificando el mensaje de la excepción
\n\nA veces no basta con verificar el tipo de excepción; también hay que asegurarse de que el mensaje contenga el texto correcto. assertThrows devuelve el objeto de la excepción, y puedes verificar sus propiedades:
import static org.junit.jupiter.api.Assertions.*;\n\n@Test\nvoid whenInvalidEmail_thenExceptionWithMessage() {\n IllegalArgumentException exception = assertThrows(\n IllegalArgumentException.class,\n () -> userService.register("invalid-email")\n );\n \n assertTrue(exception.getMessage().contains("Correo electrónico no válido"));\n}\n\n\nEste enfoque es especialmente útil al probar excepciones personalizadas con campos (por ejemplo, código de error).
\n\nMétodo 2: El clásico expected (legado de JUnit 4)
\n\nSi trabajas con JUnit 4 o mantienes código heredado, seguramente te has encontrado con el atributo expected en la anotación @Test:
@Test(expected = ArithmeticException.class)\npublic void whenDivideByZero_thenException() {\n calculator.divide(10, 0);\n}\n\n\nInconveniente: este método no permite verificar el mensaje de la excepción ni sus propiedades. Además, si la excepción se lanza antes de lo esperado (por ejemplo, en la línea de inicialización), la prueba podría pasar falsamente. En 2025, recomendamos evitar este enfoque en código nuevo.
\n\nMétodo 3: Try-catch con bandera (para fanáticos del control)
\n\nAntes de la llegada de JUnit 5, muchos usaban una construcción con try-catch y una bandera booleana. Parece engorroso, pero da control total:
@Test\nvoid whenDivideByZero_thenExceptionCaught() {\n boolean thrown = false;\n try {\n calculator.divide(10, 0);\n } catch (Arithmeti