Día 8 de 30 Fase 2: De Vanilla a la Necesidad de Herramientas

Testing Profesional en Java Puro: JUnit 5 y AssertJ

Pruebas Unitarias Rápidas, Ciclo de Vida de Maven y TDD en Java 17 y 21

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 3.5h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Java 17 LTSJava 21 LTSJUnit 5AssertJTDDTests parametrizados
Al terminar este día sabrás 4
  • Estructurar cualquier test en preparar, actuar y comprobar, con un solo comportamiento por test.
  • Escribir aserciones legibles con AssertJ sobre objetos, colecciones y excepciones.
  • Separar tests unitarios (Surefire) de tests de integración (Failsafe) en Maven.
  • Eliminar la fragilidad de los tests que dependen del orden, del estado compartido o del reloj.

El Patrón Canónico: Arrange - Act - Assert

Estructura clara de un test: Preparar los datos de entrada (Arrange), ejecutar la acción bajo prueba (Act) y comprobar el resultado esperado (Assert).

En una línea var calc = new Calculadora(); var res = calc.sumar(2, 3); assertThat(res).isEqualTo(5);

Un test con estructura AAA java
class TareaTest {

    private final Clock reloj = Clock.fixed(Instant.parse("2026-03-01T10:00:00Z"), ZoneOffset.UTC);

    @Test
    @DisplayName("completar una tarea pendiente guarda la fecha de cierre")
    void completarTareaPendiente() {
        // Arrange
        var tarea = new Tarea(1L, "Escribir tests");

        // Act
        tarea.completar(reloj);

        // Assert
        assertThat(tarea.estaCompletada()).isTrue();
        assertThat(tarea.completadaEn()).isEqualTo(Instant.parse("2026-03-01T10:00:00Z"));
    }

    @Test
    void completarDosVecesEsUnError() {
        var tarea = new Tarea(1L, "Escribir tests");
        tarea.completar(reloj);

        assertThatThrownBy(() -> tarea.completar(reloj))
            .isInstanceOf(IllegalStateException.class)
            .hasMessageContaining("ya estaba completada");
    }
}
Cómo funciona por dentro

Arrange-Act-Assert (o Given-When-Then) divide cada test en tres bloques separados por una línea en blanco. En el primero preparas los datos y colaboradores, en el segundo ejecutas exactamente una acción del código bajo prueba y en el tercero compruebas el resultado. Si necesitas varias acciones, probablemente estás probando más de un comportamiento y conviene partir el test.

El nombre del test es documentación: debe decir qué se espera y en qué situación (deberiaRechazarTituloVacio, devuelve404CuandoLaTareaNoExiste). Con @DisplayName puedes escribir frases completas que aparecen en el informe. Cuando un test falla, el nombre y la aserción deberían bastar para saber qué se rompió sin abrir el código.

Un buen test unitario es rápido (milisegundos), determinista (mismo resultado siempre), independiente (no depende de otros tests) y prueba comportamiento observable, no detalles de implementación. Si refactorizas sin cambiar el comportamiento y el test se rompe, el test estaba acoplado a la implementación.

Hace que las pruebas sean legibles, mantenibles y fáciles de diagnosticar cuando fallan.

Se conecta con JUnit 5AssertJClean Testing

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Anatomía de un Test JUnit 5 (0 - 25 min)

    Qué estudiar. Anotaciones @Test, @BeforeEach, @DisplayName y estructura AAA.

    Pregunta a responder. ¿Cómo nombrar tests con intención clara de negocio?

    Resultado esperado. Suite de pruebas para una calculadora financiera con nombres legibles.

  2. Bloque 2: Dominio de AssertJ (25 - 50 min)

    Qué estudiar. Aserciones sobre colecciones, nulos, strings y excepciones.

    Pregunta a responder. ¿Cómo comprobar en una sola línea que una lista tiene 3 elementos y no tiene nulos?

    Resultado esperado. Pruebas con assertThat() y assertThatThrownBy().

  3. Bloque 3: Compilación y Maven Wrapper (50 - 80 min)

    Qué estudiar. Ciclo de Maven, ejecución con ./mvnw test y dependencias de test en pom.xml.

    Pregunta a responder. ¿Por qué el scope <scope>test</scope> es vital para no inflar el JAR final?

    Resultado esperado. Ejecución limpia de la suite por terminal con Maven Wrapper.

  4. Cierre y Rúbrica (80 - 90 min)

    Qué estudiar. Repaso activo de los 5 conceptos clave de testing.

    Pregunta a responder. Responder mentalmente la pregunta del tutor sobre Clock y fechas fijas.

    Resultado esperado. Progreso y kata de testing marcada en la base de datos de estudio.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué un test unitario puro debe tardar milisegundos

Lo que debes entender

Un test unitario prueba solo lógica de Java en memoria. Si un test tarda 2 segundos, no es unitario; está abriendo red, archivos o levantando un framework pesado.

Analogía para contarlo

Comprobar el cálculo de una nómina en una hoja de papel (test unitario en memoria) frente a tener que enviar la carta al banco y esperar el extracto mensual (test de integración).

Lo que suele explicarse mal

Llamar "test unitario" a pruebas que levantan bases de datos completas o servidores web.

Señal de que aún no lo dominas

Si necesitas poner @SpringBootTest para probar una clase de cálculo matemático.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué los métodos de test en JUnit 5 no necesitan ser public como en JUnit 4? Ver respuesta

Respuesta razonada

JUnit 4 necesitaba métodos public porque su runner los descubría y ejecutaba con reflexión sin cambiar la accesibilidad. JUnit 5 (Jupiter) descubre los métodos de test y los invoca habilitando el acceso por reflexión, así que le basta con que no sean private.

La recomendación oficial es usar visibilidad de paquete para clases y métodos de test: menos ruido y deja claro que no forman parte de ninguna API. Los métodos private sí se ignoran, porque Jupiter considera que no deben ejecutarse como tests.

Qué está evaluando la pregunta

Porque JUnit 5 utiliza reflexión moderna de Java que puede invocar métodos con visibilidad de paquete (package-private).

Nivel 2 (Intermedio) ¿Cuál es la diferencia entre assertAll() en JUnit 5 y múltiples aserciones consecutivas? Ver respuesta

Respuesta razonada

Con aserciones consecutivas, la primera que falla lanza un AssertionError y el método termina: no sabes si las siguientes también habrían fallado. Con assertAll(...) JUnit ejecuta todas las aserciones del grupo, recoge los fallos y lanza un único MultipleFailuresError que los lista todos.

Es útil cuando verificas varias propiedades independientes de un mismo resultado, por ejemplo los campos de un DTO. AssertJ ofrece lo mismo con SoftAssertions. No conviene abusar: si la segunda aserción no tiene sentido cuando falla la primera (por ejemplo, comprobar un campo de un objeto que es null), las aserciones consecutivas son lo correcto.

Qué está evaluando la pregunta

Las aserciones consecutivas se detienen en el primer fallo. assertAll() ejecuta todas las aserciones y reporta todos los fallos juntos.

Nivel 3 (Entrevista Senior) ¿Cómo pruebas con JUnit 5 un método que depende de la fecha actual (Instant.now()) sin que el test sea inestable en diferentes días o zonas horarias? Ver respuesta

Respuesta razonada

No llamando a Instant.now() o LocalDate.now() directamente en el código de negocio. El reloj es una dependencia más: se inyecta un java.time.Clock en el constructor y se usa Instant.now(clock). En producción pasas Clock.systemUTC() (en Spring, un @Bean Clock); en los tests, Clock.fixed(Instant.parse("2026-03-01T10:00:00Z"), ZoneOffset.UTC).

Así el test es determinista en cualquier día y zona horaria, y además puedes probar casos difíciles como un cambio de día, el 29 de febrero o el cambio de horario, simplemente fijando el reloj en ese instante.

Qué está evaluando la pregunta

Inyectando un java.time.Clock en el constructor de la clase, permitiendo que el test le pase un Clock.fixed(instante, zona) inmutable.

Reto del tutor

Un programador escribe un test sin ninguna aserción, diciendo: "Si el método corre y no lanza excepción, el test pasa a verde y significa que funciona". Evalúa esta práctica.

Lo que una respuesta suele tener bien

Nota que efectivamente el test pasará a verde si no hay excepción.

Lo que suele quedarse en la superficie

Decir que "hace falta un assert porque es la regla de JUnit".

El concepto de fondo que suele faltar

No entender que el test no está validando que el resultado sea correcto ni que el estado del sistema haya cambiado adecuadamente.

Cómo lo respondería alguien senior en una entrevista

Es un falso test ("smoke test ciego") que genera una falsa sensación de cobertura. El método podría devolver null, una lista vacía o valores corruptos y el test seguiría en verde. Un test unitario riguroso debe validar el valor de retorno exacto o los efectos colaterales mediante aserciones explícitas de AssertJ.

Proyecto «Pedidos» · paso 8 de 30

Tests del dominio

Proteger las reglas de negocio con tests rápidos que se lean como una especificación.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Escribe tests de todas las transiciones de estado del pedido, las permitidas y las prohibidas.
  2. Prueba el cálculo de totales y comisiones con @ParameterizedTest y @CsvSource.
  3. Comprueba las excepciones con assertThatThrownBy, incluido su mensaje.

Está listo cuando

  • Los tests del dominio tardan menos de un segundo en total.
  • Si cambias una regla de transición, al menos un test falla y dice cuál.

Katas: ejercicios cortos

Reglas de oro

  • Estructura siempre tus tests en Arrange - Act - Assert.
  • Cada test debe ser 100% independiente del orden de ejecución.
  • Inyecta java.time.Clock para probar lógica temporal sin sorpresas.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown