Introducción a JUnit en Java
Qué es JUnit, cómo añadirlo a un proyecto Java y cómo escribir tu primer test con JUnit.
JUnit es el framework estándar de facto para escribir pruebas automatizadas en Java. Un test es simplemente un método pequeño que ejecuta una parte de tu código y verifica que se comportó como se esperaba; el trabajo de JUnit es descubrir esos métodos, ejecutar cada uno de forma aislada, comprobar las aserciones e informar cuáles pasaron y cuáles fallaron. La generación actual, JUnit 5 (también llamada JUnit Jupiter), se distribuye como un conjunto de pequeñas librerías que añades a tu build — no forma parte del JDK — y potencia el paso mvn test / gradle test que es la puerta de entrada a la CI de casi cualquier proyecto Java.
Por qué un framework de testing
Podrías verificar el código manualmente con métodos main y println — pero eso escala mal. Un framework te da cuatro cosas que de otro modo tendrías que reconstruir tú mismo:
- Descubrimiento — encuentra automáticamente cada método
@Test; nunca tienes que mantener una lista. - Aislamiento — cada test obtiene un fixture nuevo, para que un test no pueda corromper a otro.
- Aserciones — un vocabulario rico (
assertEquals,assertThrows, …) que produce mensajes de fallo precisos. - Informes — un resumen uniforme de pasa/falla que la herramienta de build y el IDE entienden.
Haz esto bien una vez y los tests se vuelven baratos de escribir, que es el punto principal: los tests baratos se escriben, y el código que está testeado puede modificarse sin miedo.
Añadir JUnit a un proyecto
JUnit 5 es una dependencia, declarada en tu archivo de build. Con Maven, el agregador junit-jupiter incluye la API (para compilar) y el motor (para ejecutar):
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>Con Gradle son dos líneas más el interruptor useJUnitPlatform():
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
useJUnitPlatform()
}Los fuentes de test viven bajo src/test/java, reflejando el paquete de la clase que prueban. El scope test mantiene JUnit fuera de tu artefacto de producción.
Tu primer test
Un test de JUnit es un método ordinario anotado con @Test. Dentro de él llamas al código bajo prueba y realizas aserciones sobre el resultado. Aquí tienes una clase Calculator y una clase de test para ella:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CalculatorTest {
private final Calculator calc = new Calculator();
@Test
void addReturnsSum() {
assertEquals(5, calc.add(2, 3));
}
@Test
void divideByZeroThrows() {
assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
}
}Observa el patrón que sigue cada test — Arrange (preparar) el fixture, Act (actuar) llamando al método, Assert (verificar) el resultado. Los métodos de test son de acceso de paquete (no se necesita public en JUnit 5) y devuelven void. Ejecuta mvn test y el build pasa a verde solo si todas las aserciones se cumplen.
Las anotaciones y aserciones que más usarás
La superficie de JUnit es pequeña. Estos pocos miembros cubren la gran mayoría de los tests reales:
| Miembro | Paquete / clase | Propósito |
|---|---|---|
@Test | org.junit.jupiter.api | Marca un método como test |
@BeforeEach / @AfterEach | org.junit.jupiter.api | Se ejecuta antes/después de cada test (preparar / limpiar fixtures) |
@BeforeAll / @AfterAll | org.junit.jupiter.api | Se ejecuta una vez antes/después de todos los tests de la clase |
@DisplayName | org.junit.jupiter.api | Un nombre legible para los informes |
@Disabled | org.junit.jupiter.api | Omite temporalmente un test |
assertEquals(exp, act) | Assertions | Falla si los dos no son iguales |
assertTrue / assertFalse | Assertions | Falla si un boolean no se cumple |
assertThrows(type, exec) | Assertions | Falla si la lambda no lanza esa excepción |
assertNull / assertNotNull | Assertions | Falla si la nulabilidad es incorrecta |
@BeforeEach es lo que le da a cada test un estado limpio — JUnit construye una instancia nueva de la clase de test para cada @Test, luego ejecuta la configuración, por lo que el estado nunca se filtra entre tests.
Lo que JUnit hace por ti, en un archivo ejecutable
El ejecutor de código aquí no tiene JUnit en su classpath (es una librería externa, no parte del JDK), por lo que el ejemplo siguiente reimplementa el bucle principal de JUnit en Java puro: un fixture recreado antes de cada test, un pequeño conjunto de helpers assertXxx, una lista de métodos de test ejecutados de forma independiente y un recuento de pasa/falla al final. Esta es exactamente la maquinaria que JUnit automatiza — verla al descubierto hace que el framework real resulte obvio. Un test falla a propósito para que puedas ver cómo se ve el rojo.
Lo que hay que extraer de la ejecución:
- Los tres tests correctos imprimen
PASSy el cuarto imprimeFAIL deliberatelyFailing -> expected <10> but was <5>— un mensaje de fallo preciso, no solo "un test falló." Esa diferencia (expected … but was …) es exactamente lo que elassertEqualsde JUnit te proporciona, y es lo que hace que un test rojo sea diagnosticable de un vistazo. setUp()se ejecuta antes de cada test, por lo quecalces una instancia nueva deCalculatorcada vez. Este es el contrato de@BeforeEach: los tests están aislados, y el orden en que se ejecutan nunca puede importar porque ninguno de ellos comparte estado mutable.divideThrowsOnZeropasa verificando que una excepción sí se lanza —assertThrowsconvierte "esto debería fallar" en una aserción positiva de primera clase en lugar de un frágil try/catch. Las excepciones esperadas son comportamiento que vale la pena probar, no errores que ignorar.- El recuento final —
Tests run: 4, Passed: 3, Failed: 1, Assertions: 5— es el informe. Un test fallido de cuatro aún cambia todo el build aRED; la CI trata cualquier fallo como una parada, por eso una suite en verde es significativa. - Nada aquí importó JUnit, sin embargo la forma es idéntica: descubrimiento de métodos anotados, configuración por test, aserciones, resumen. El valor de JUnit es que automatiza este bucle (y añade descubrimiento, paralelismo, tests parametrizados e integración con el IDE) para que tú solo escribas los cuerpos de los tests.
Qué cubre el resto de esta parte
Esta parte profundiza en el bucle central que acabas de ver:
- Anotaciones de JUnit —
@Test,@DisplayName,@Disabledy el resto del conjunto de marcadores. - El ciclo de vida del test — cómo
@BeforeEach/@AfterEach/@BeforeAll/@AfterAllle dan a cada test un fixture limpio. - Aserciones — el catálogo completo de
assertXxx, incluyendoassertThrowspara pruebas de excepciones. - Tests parametrizados — ejecutar un cuerpo de test con muchas entradas en lugar de copiar y pegar casos.
- Mocking con Mockito — reemplazar los colaboradores de una clase con sustitutos para que un test unitario siga siendo un test unitario.
Si quieres tener el panorama general de por qué importan las pruebas automatizadas antes de sumergirte en la API, consulta Testing en Java. De lo contrario, el siguiente capítulo comienza donde empieza toda suite: definir una clase de test y ejecutarla.