CodeGym /Cursos /Spring Boot /JUnit y el esqueleto de pruebas en Spring Boot

JUnit y el esqueleto de pruebas en Spring Boot

Spring Boot
Nivel 26, Lección 1
Disponible

1. spring-boot-starter-test: conjunto de pruebas

El seguro de inicio no aparece por sí solo: el proyecto necesita un marco de pruebas que sepa ejecutar las pruebas y, cuando sea necesario, levantar el contexto de Spring. En las pruebas, un principiante suele encontrarse con dos extremos: o bien «las pruebas dan miedo, no las toco», o bien «ahora conectaré diez bibliotecas y ellas mismas lo probarán todo». Spring Boot (como siempre) ofrece un camino más tranquilo: tomamos un starter consensuado y obtenemos una pila básica funcional. Esto es importante no porque «esté de moda», sino porque las versiones y la compatibilidad ya han sido cuidadosamente ajustadas por usted.

Si simplificamos a nivel humano: spring-boot-starter-test es un conjunto seleccionado de bibliotecas de pruebas que Boot elige para que sean compatibles en versiones y funcionen juntas. Usted conecta un solo starter, y no manualmente un «jenga» de dependencias donde una pieza incorrecta y toda la noche leyendo NoSuchMethodError.

En build.gradle.kts se ve de manera muy sencilla:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webmvc")
    implementation("org.springframework.boot:spring-boot-starter-actuator")

    // Importante: esta dependencia solo es necesaria para las pruebas y no aparecerá en el runtime-classpath de producción
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Observe la palabra testImplementation. No es simplemente «otro nombre de implementation», sino un significado bastante importante: esta dependencia solo es necesaria para las pruebas y no debe formar parte del runtime-classpath principal de la aplicación. Es decir, su artefacto de producción no arrastra consigo elementos de prueba (ni se convierte en un «frasco con sorpresa»).

¿Qué trae exactamente spring-boot-starter-test? A nivel de esta lección nos basta con entender el panorama general: proporciona JUnit 6 (Jupiter API), mecanismos básicos de assertion e integración con las pruebas de Spring (cuando necesite levantar el contexto). Las demás bibliotecas puede considerarlas como «vienen incluidas, pero hoy no es necesario conocerlas de memoria».

Aquí tiene un «mapa del paquete» en forma de tabla (sin intentar aprenderlo todo de una vez):

Qué viene incluido en el starter Para qué sirve en la vida cotidiana
JUnit 6 (Jupiter API) Ejecuta las pruebas, entiende @Test, muestra informes.
Spring Test / Spring Boot Test Permite levantar el contexto de Spring/Boot en las pruebas cuando realmente es necesario.
AssertJ (normalmente también está) Comprobaciones más expresivas que las aserciones básicas de JUnit (se puede vivir sin ello al principio).
Mockito (normalmente también está) Stubs/mocks para aislar parte de las dependencias (en esta lección no profundizamos).
Un poco de soporte para pruebas JSON Útil para la capa web, pero hoy nos centramos en el marco básico.

La idea principal: el starter no es para que usted se convierta en «la persona que conoce 40 bibliotecas de pruebas», sino para que pueda empezar a escribir pruebas comprensibles sin sufrir con las dependencias.

2. Dónde viven las pruebas: src/test/java

Cuando ve src/test/java por primera vez, es muy tentador tratarlo como una «carpeta para cualquier cosa» donde se puede tirar lo que sea. Pero en Gradle (y en general en el mundo Java) no es solo una carpeta, sino un test source set aparte. Tiene su propio classpath, sus propias dependencias y sus propias reglas de compilación, por lo tanto, su propio significado. Esta separación ayuda a mantener el proyecto limpio: el código de producción no se contamina con herramientas de prueba.

La estructura del proyecto por source sets suele verse así:

src/
├── main/
│   ├── java/          # código de la aplicación de producción
│   └── resources/     # application.yaml, static/, etc.
└── test/
    ├── java/          # código de prueba
    └── resources/     # recursos de prueba (si se necesitan)

La idea es simple: todo lo que está en src/main/java es con lo que se construye su aplicación. Todo lo que está en src/test/java es lo que ayuda a comprobar la aplicación, pero no forma parte del «producto».

Un detalle muy práctico que ahorra nervios: las pruebas suelen repetir la estructura de paquetes del código de producción. No porque «así lo exige GOST», sino porque así es más fácil orientarse. Si los modelos de dominio están en com.example.catalogservice.catalog.domain, entonces las pruebas de esos modelos tienen sentido colocarlas en el mismo paquete, pero dentro de src/test/java.

Otro punto importante: las pruebas se compilan y ejecutan mediante una tarea separada de Gradle. Por lo general, se ejecuta:

./gradlew test

Y dentro de ese proceso, Gradle compila las pruebas, inicia el runtime de pruebas, ejecuta JUnit Jupiter y recopila el informe. Si le gustan los diagramas (y al cerebro le gustan), puede representarlo así:

flowchart TD
    A["./gradlew test"] --> B["Compilación de src/main y src/test"]
    B --> C["JUnit 6 ejecuta métodos @Test"]
    C --> D["assertions (sus comprobaciones)"]
    D -->|OK| E["Prueba verde + informe"]
    D -->|Fail/Exception| F["Prueba roja + informe"]

En resumen, src/test/java no es un «cementerio de fragmentos de código», sino un mecanismo de calidad integrado que vive junto a la aplicación y recuerda regularmente: «¿estás seguro de que no has roto nada?».

3. JUnit Jupiter y @Test

Si Spring Boot es la «plataforma para la aplicación», JUnit es la «plataforma para ejecutar las pruebas». Y la buena noticia es que el modelo básico de JUnit es muy simple: tiene una clase de prueba, dentro un método de prueba, y está marcado con @Test. Todo. Sin public static void main, sin «runners» especiales, sin rituales con panderetas (al menos en el nivel inicial).

JUnit Jupiter es la versión moderna de JUnit (lo que normalmente se llama JUnit 6). En el mundo de Boot ya es el valor predeterminado desde hace tiempo, y es sobre él que construimos el marco de pruebas del proyecto.

Una prueba mínima de una entidad de dominio catalog-service podría ser así. Supongamos que tiene un enum CourseLevel en el paquete catalog.domain.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CourseLevelTest {

    @Test
    void basicLevelNameIsStable() {
        // esperado: el nombre de cadena del enum está fijo y no debe cambiar «accidentalmente»
        // actual: lo que realmente devuelve name()
        assertEquals("BASIC", CourseLevel.BASIC.name());
    }
}

Aquí hay varios puntos importantes que conviene «sentir» con un ejemplo tan sencillo.

En primer lugar, el método con @Test no es «solo un método», sino un contrato: JUnit lo encontrará, lo ejecutará y decidirá si la prueba pasa o no. En segundo lugar, el nombre del método puede ser casi cualquier cosa (con tal de que sea un identificador Java válido). Habitualmente se usa un estilo que se lee como una frase: basicLevelNameIsStable, parsesEnumValue, throwsOnInvalidInput.

En tercer lugar, la clase de prueba no tiene por qué ser public. Incluso es más cómodo: menos tentación de «empezar a usar las pruebas como una biblioteca accidentalmente». Las pruebas no son una API, las pruebas son un seguro.

Y un detalle más que sorprende a los principiantes: la prueba no debe imprimir nada. La prueba debe pasar en silencio o fallar honestamente. Si ve muchos System.out.println en las pruebas, normalmente es señal de que la prueba aún está actuando como depurador, no como verificación. El depurador también es útil, pero no sustituye a las aserciones.

4. Assertions en JUnit

El «engaño» más frecuente al empezar con las pruebas es así: una persona escribe una prueba, la ejecuta, está verde… y eso no significa absolutamente nada. ¿Por qué? Porque la prueba estaba «vacía»: no comprobaba nada. Las aserciones (assertions) son como la pregunta de un profesor en un examen: no importa lo seguro que llegue al aula, lo importante es que pueda responder a una pregunta concreta.

JUnit proporciona un conjunto de aserciones básicas que son más que suficientes para empezar. Lo principal es entender su significado y aprender a leer las firmas.

Una pequeña tabla de «qué comprobar con qué»:

Aserción Qué comprueba Ejemplo de situación
assertEquals(expected, actual) Los valores son iguales Espera un nombre de enum o un número concreto
assertNotNull(value) El valor no es null Ha obtenido un objeto y quiere asegurarse de que realmente se ha creado
assertTrue(condition) La condición es verdadera Comprueba que «un indicador está activado», «la lista no está vacía»
assertFalse(condition) La condición es falsa Comprueba que «un indicador está desactivado»
assertThrows(type, executable) Al ejecutar el código se lanza una excepción Comprueba el comportamiento de fail‑fast ante una entrada incorrecta

assertEquals: «esperado» y «real»

La comprobación más clásica: comparar dos valores. Es importante no confundir el orden de los argumentos: primero expected, luego actual. De lo contrario, al fallar, obtendrá un mensaje difícil de leer.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CourseTrackTest {

    @Test
    void valueOfParsesEnumName() {
        // Preparación: obtenemos el enum a partir de una cadena (como si viniera de la configuración)
        CourseTrack track = CourseTrack.valueOf("SPRING");

        // Comprobación: esperamos un elemento enum concreto
        assertEquals(CourseTrack.SPRING, track);
    }
}

Aquí no estamos probando Spring, ni Boot, ni el «contenedor». Estamos probando la parte pura de Java: el comportamiento del enum y la previsibilidad de los valores de cadena que luego podrían venir de la configuración.

assertNotNull: «el objeto realmente existe»

Esta comprobación es trivial, pero útil cuando se empieza. Ayuda a detectar situaciones en las que algo «no se creó» o «no se devolvió», y luego no se cae en un NullPointerException en cualquier sitio.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class MoneyTest {

    @Test
    void moneyHasCurrency() {
        // Arrange: creamos un value‑object
        Money price = new Money(19900, "GBP");

        // Assert: el objeto se ha creado y contiene la moneda esperada
        assertNotNull(price);
        assertEquals("GBP", price.currency());
    }
}

Sí, parece obvio. Pero las pruebas a menudo deben ser obvias: su trabajo no es sorprender, sino proteger contra accidentes tontos.

assertTrue / assertFalse: «comprobamos una condición, no un valor concreto»

A veces no quiere comparar dos números concretos, sino comprobar una propiedad o condición. Por ejemplo: «la cadena no está vacía», «la suma no es negativa», «el indicador está desactivado».

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CourseCardTest {

    @Test
    void courseSlugLooksNonEmpty() {
        // Datos para la prueba (en el ejemplo didáctico, lo más cortos posible)
        CourseCard card = new CourseCard(
                "spring-boot",
                "spring-boot",
                "Spring Boot"
        );

        // Comprobamos dos propiedades de slug a la vez:
        // 1) parece «similar a nuestro formato»
        assertTrue(card.slug().startsWith("spring"));
        // 2) definitivamente no está vacío/solo espacios
        assertFalse(card.slug().isBlank());
    }
}

Aquí muestro intencionadamente una versión corta de CourseCard (con tres campos), porque en el código didáctico a veces es útil tener un constructor/record simplificado para las demostraciones. Si en su proyecto CourseCard contiene más campos, la idea no cambia: usted comprueba una propiedad concreta, no «todo a la vez».

assertThrows: «esperamos un fallo y lo consideramos un éxito»

assertThrows es una forma de convertir una «prueba roja» en una intención significativa. En lugar de «la prueba ha fallado accidentalmente porque todo se ha roto», usted dice: «aquí debería haber una excepción, y es normal».

El ejemplo más sencillo, relacionado con nuestro dominio (y muy parecido a los dolores reales de configuración): una cadena incorrecta no debe «convertirse silenciosamente en algo», debe ser un error notable.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;

class CourseTrackParsingTest {

    @Test
    void valueOfThrowsOnWrongCase() {
        // Importante: pasamos a assertThrows «código ejecutable», no lo llamamos de antemano.
        // Aquí esperamos IllegalArgumentException, porque enum valueOf distingue mayúsculas y minúsculas.
        assertThrows(IllegalArgumentException.class, () ->
                CourseTrack.valueOf("spring"));
    }
}

Observe la lambda () -> .... No llamamos al método directamente, pasamos a JUnit un «fragmento de código» que debe ejecutar y comprobar que falla con la excepción esperada.

5. Pruebas JUnit y @SpringBootTest

Hasta aquí hemos construido exactamente un marco JUnit normal. Y ya es útil: enum, value‑object, una pequeña función, parsing, fail‑fast en Java puro: todo eso se comprueba rápido y sin Spring. Para esas cuestiones, @Test y las aserciones son más que suficientes.

Pero un proyecto Boot tiene una clase aparte de roturas: la aplicación puede no construir el contexto en absoluto. Allí ya no basta con llamar a un método. Hay que comprobar que el servicio como sistema se levanta, que el wiring no se ha deshecho, que la configuración no se ha peleado con el código. Para eso existen las pruebas de contexto con Spring.

Un límite útil y simple: si la pregunta suena como «¿esta lógica Java funciona?», basta con JUnit simple. Si la pregunta suena como «¿la aplicación se compila y arranca en conjunto?», entonces comienza el nivel del contexto de Boot.

6. Errores típicos al escribir pruebas

Las primeras pruebas suelen escribirse en modo «aún no estoy seguro de lo que hago», y es normal. Los errores aquí no suelen ser sobre «conceptos complejos», sino sobre pequeños hábitos que o bien rompen el sentido de la prueba o la hacen demasiado costosa. La buena noticia: casi todos estos errores se curan con un par de principios simples y atención.

Error nº1: la prueba existe, pero no hay aserciones.
El principiante escribe un método con @Test, lo ejecuta, ve el resultado verde y se alegra. Pero luego de repente se da cuenta de que la prueba no comprobaba nada: simplemente se ejecutó. Estas pruebas crean una falsa sensación de seguridad. Si quiere «comprobar un hecho», ese hecho debe expresarse con assertEquals, assertTrue o al menos assertNotNull.

Error nº2: confusión entre src/main/java y src/test/java.
A veces las clases de prueba acaban por error en src/main/java porque el IDE «creó automáticamente la clase en otro sitio». Esto tiene consecuencias desagradables: el código de prueba puede terminar en el artefacto de producción, y las dependencias de prueba empiezan a considerarse obligatorias para el runtime. El hábito correcto es simple: cualquier clase que exista solo para la comprobación debe vivir en src/test/java.

Error nº3: @SpringBootTest se usa en todas partes.
Este es un típico «error de Boot»: ya que Spring sabe levantar el contexto, entonces lo haremos siempre así. Como resultado, las pruebas se vuelven lentas y usted empieza a ejecutarlas con menos frecuencia. Y ejecutar las pruebas con menos frecuencia es una forma muy segura de que se conviertan en un elemento decorativo. El contexto es necesario cuando se comprueba la construcción de la aplicación, el wiring, la configuración. Para la lógica Java pura basta con JUnit normal.

Error nº4: las dependencias de prueba se añaden como implementation.
Si por descuido se añade spring-boot-starter-test no como testImplementation, sino como implementation, se arrastra la pila de pruebas al classpath de producción. No es el fin del mundo, pero es una mala higiene del proyecto y un «desdibujamiento del límite» entre lo que necesita la aplicación y lo que necesitan las comprobaciones. Un proyecto Boot ya tiene suficientes dependencias; no añadamos también las de prueba en producción.

Error nº5: assertEquals está escrito en el orden incorrecto.
Cuando en assertEquals se intercambian expected y actual, al fallar la prueba se obtiene un mensaje difícil de leer: parece que se esperaba una cosa y se obtuvo otra, pero usted mismo lo ha confundido. Es un detalle, pero los detalles en las pruebas son como la arena en el teclado: al principio «bueno, una tontería», y luego las teclas empiezan a chirriar.

1
Tarea
Spring Boot, nivel 26, lección 1
Bloqueada
Primer test JUnit sin contexto de Spring
Primer test JUnit sin contexto de Spring
1
Tarea
Spring Boot, nivel 26, lección 1
Bloqueada
Esqueleto basico de test de Spring con ApplicationContext
Esqueleto basico de test de Spring con ApplicationContext
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION