Ir al contenido

Tutorial: Pruebas automáticas del Service con la base de datos

Al terminar este tutorial vas a ser capaz de:

  • Configurar un proyecto Spring Boot 4 para escribir pruebas del Service contra una base de datos real (H2).
  • Escribir un <Entidad>ServiceTest con @DataJpaTest, TestEntityManager y un @BeforeEach que deja la base en un estado conocido.
  • Generar los datos de prueba con Instancio, y forzar valores válidos o inválidos según el caso.
  • Cubrir cada método del Service y cada regla de negocio con un caso feliz y un caso de error.
  • Comprobar que tus pruebas detectan un error: romper una regla a propósito y ver que alguna prueba falla.

Prerrequisitos: el Service de una entidad, porque estas pruebas ejercitan ese código, y la teoría de Pruebas de lógica de negocio.


En el tutorial del Service escribiste esa capa, pero todavía no la has probado con una prueba automática, y no hace falta el Controller para hacerlo: el Service se prueba por sí solo. Una prueba automática compara el resultado con lo esperado y, si no coincide, falla, sin que tengas que leer nada en la consola. Corre cada vez que ejecutas ./mvnw test, y también en el Jenkins del equipo.

Dos decisiones de este curso:

  • Se prueba con la base de datos real, no con un doble (Mockito). Así las pruebas comprueban lo que de verdad pasa al guardar y consultar, incluido lo que hace Spring Data con tus métodos del repository.
  • Un archivo de prueba por entidad: ArtistaServiceTest prueba los métodos de ArtistaService. Las relaciones entre entidades tendrán su propio archivo (por ejemplo ArtistaAlbumServiceTest) cuando escribas el Service de asociaciones.

El ejemplo prueba el ArtistaService del tutorial del Service. Cuando lo tengas, repites el patrón para las demás entidades de tu módulo con tus reglas.


Abre el pom.xml y agrega, junto a las demás dependencias de prueba, estas dos:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>5.4.1</version>
<scope>test</scope>
</dependency>
  • spring-boot-starter-data-jpa-test trae @DataJpaTest y TestEntityManager. Desde Spring Boot 4 no vienen con el starter de pruebas general: sin esta dependencia, esas clases no existen.
  • instancio-junit es la librería que fabrica los datos de prueba.

Si tu proyecto tiene un data.sql con datos de ejemplo, @DataJpaTest lo ejecuta también sobre su base de pruebas. Entonces un findAll devuelve esos registros además de los que crea cada prueba, y los conteos no coinciden. Para que las pruebas partan de una base vacía, la anotación lleva la propiedad spring.sql.init.mode=never, como verás en el siguiente paso.


Crea ArtistaServiceTest.java en src/test/java, en el mismo paquete que ArtistaService:

package co.edu.uniandes.musical.artistas;
import co.edu.uniandes.musical.exception.EntityNotFoundException;
import co.edu.uniandes.musical.exception.IllegalOperationException;
import static org.instancio.Select.field;
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.instancio.Instancio;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
import org.springframework.context.annotation.Import;
@DataJpaTest(properties = "spring.sql.init.mode=never")
@Import(ArtistaService.class)
class ArtistaServiceTest {
@Autowired
private ArtistaService artistaService;
@Autowired
private TestEntityManager entityManager;
private List<ArtistaEntity> artistaList = new ArrayList<>();
}

Las piezas son las de la teoría:

  • @DataJpaTest arranca solo la parte de JPA, sin servidor web, con una base H2 en memoria. Con properties se desactiva el data.sql.
  • La transacción por prueba ya viene con @DataJpaTest: cada prueba corre dentro de una transacción que se revierte al terminar, así que lo que una prueba guarda no lo ve la siguiente. No hace falta anotar la clase con @Transactional.
  • @Import(ArtistaService.class) agrega al contexto el Service que se va a probar. @DataJpaTest no lo carga por sí solo, porque solo registra la capa de persistencia.
  • TestEntityManager sirve para preparar y verificar la base sin pasar por el Service que estás probando. Si el Service tuviera un error, también dañaría la preparación de los datos y la prueba no sabría qué falló.
  • artistaList guarda los artistas que se insertan antes de cada prueba, para que las pruebas puedan usarlos.
  • Los dos imports de exception son las excepciones del Service, que viven en el paquete compartido co.edu.uniandes.musical.exception. Si en tu proyecto el paquete se llama distinto (por ejemplo exceptions), usa el tuyo. Las pruebas de las secciones siguientes las usan con assertThrows.

5. Preparar los datos: @BeforeEach e Instancio

Sección titulada «5. Preparar los datos: @BeforeEach e Instancio»

JUnit no garantiza en qué orden corren las pruebas, así que cada una tiene que arrancar con un estado conocido. Agrega esto dentro de la clase:

@BeforeEach
void setUp() {
clearData();
insertData();
}
private void clearData() {
entityManager.getEntityManager()
.createQuery("delete from ArtistaEntity")
.executeUpdate();
}
private void insertData() {
for (int i = 0; i < 3; i++) {
ArtistaEntity artista = crearArtista();
entityManager.persist(artista);
artistaList.add(artista);
}
}
private ArtistaEntity crearArtista() {
return Instancio.of(ArtistaEntity.class)
.ignore(field(ArtistaEntity::getId))
.ignore(field(ArtistaEntity::getAlbumes))
.generate(field(ArtistaEntity::getFechaNacimiento),
gen -> gen.temporal().localDate()
.range(LocalDate.of(1940, 1, 1), LocalDate.now().minusYears(20)))
.create();
}
  • setUp se ejecuta antes de cada @Test: si hay quince pruebas, corre quince veces. Borra lo que haya e inserta tres artistas nuevos.
  • La limpieza es una consulta JPQL (delete from ArtistaEntity), no SQL: se escribe con el nombre de la clase, no el de la tabla.
  • crearArtista() es el único lugar donde se decide cómo se fabrica un artista de prueba. Instancio llena todos los atributos con valores aleatorios (nombre, país, biografía…). Si mañana ArtistaEntity gana un atributo, ninguna prueba cambia.
  • ignore(field(...)) deja un atributo sin valor. El id se ignora porque lo asigna la base de datos, y los albumes porque Instancio fabricaría álbumes inventados que no existen en la base. Si tu entidad tiene otras relaciones, ignóralas igual.
  • field(ArtistaEntity::getId) se escribe con una referencia al método. Si renombras el atributo, el compilador te avisa.
  • generate(field(...), ...) pide un valor aleatorio, pero dentro de lo que tu regla acepta. El Service exige que, si se indica la fecha de nacimiento, el artista sea mayor de edad, así que la fecha se genera entre 1940 y hace 20 años, con margen sobre el mínimo de 18. Por qué hace falta, en la subsección siguiente.

Datos válidos: fija lo que tus reglas validan

Sección titulada «Datos válidos: fija lo que tus reglas validan»

Instancio no conoce las reglas de tu Service: llena cada atributo con un valor al azar. Mientras el Service no valide ese atributo, no importa. Pero si lo valida, el valor aleatorio casi nunca cumple la regla, y el efecto es traicionero:

  • Un texto aleatorio nunca es una URL. Si tu Service exigiera que la foto lo sea, todos los create de tus pruebas fallarían, y por el dato, no por el código.
  • Una fecha aleatoria cae en cualquier año. Con la regla de la mayoría de edad, en una medición con 2000 fechas generadas por Instancio, cerca de dos de cada tres la incumplían. Entonces la prueba del caso feliz pasa unas veces y falla otras, sin que el código haya cambiado. Es la peor falla: una prueba intermitente hace que nadie confíe en las demás.

La regla es que todo atributo que una regla valida se fija, dentro de lo válido, en el único lugar donde se fabrica un artista: la fábrica de datos (crearArtista()). Hay tres formas de fijarlo, según la regla:

La regla es… Se fija con…
Un formato (una URL) set: un valor fijo y válido
Un rango (la fecha de nacimiento) generate: sigue siendo aleatorio, pero dentro del rango
Algo que depende de otro atributo (el debut, después del nacimiento) un setter después de create(), calculado a partir del otro atributo

Así quedaría tu crearArtista() si tu entidad tuviera más reglas que las de este tutorial (la foto con formato y un debut posterior al nacimiento). Es un modelo para que veas las tres técnicas juntas, no un cambio que debas hacer ahora:

private ArtistaEntity crearArtista() {
ArtistaEntity artista = Instancio.of(ArtistaEntity.class)
.ignore(field(ArtistaEntity::getId))
.ignore(field(ArtistaEntity::getAlbumes))
.set(field(ArtistaEntity::getFotoArtistaUrl), "https://example.com/foto.jpg")
.generate(field(ArtistaEntity::getFechaNacimiento),
gen -> gen.temporal().localDate()
.range(LocalDate.of(1940, 1, 1), LocalDate.now().minusYears(20)))
.create();
artista.setFechaDebut(artista.getFechaNacimiento().plusYears(20));
return artista;
}

Fíjate en los márgenes. El rango termina hace 20 años y no hace 18, para que el artista de prueba no quede en el límite de la regla. Y el debut, 20 años después del nacimiento, nunca queda en el futuro, porque el nacimiento es de hace 20 años o más.

Los casos de error parten de ese artista válido y rompen una sola cosa, como verás en la sección 7:

ArtistaEntity nuevo = crearArtista();
nuevo.setFotoArtistaUrl("esto no es una url");

Así, si la prueba falla por otra razón, sabes que el dato válido no es la causa. Y si una prueba del caso feliz falla a veces, sospecha de un atributo aleatorio que tu Service valida (hay una entrada en Errores frecuentes).


Escribe esta primera prueba a mano, sin pedirle a una IA que la genere: sirve para entender el patrón con el que vas a escribir todas las demás.

@Test
void testCreateArtista() throws Exception {
ArtistaEntity nuevo = crearArtista();
ArtistaEntity resultado = artistaService.create(nuevo);
assertNotNull(resultado.getId());
entityManager.flush();
entityManager.clear();
ArtistaEntity guardado =
entityManager.find(ArtistaEntity.class, resultado.getId());
assertEquals(nuevo.getNombre(), guardado.getNombre());
assertEquals(nuevo.getFotoArtistaUrl(), guardado.getFotoArtistaUrl());
assertEquals(nuevo.getPaisOrigen(), guardado.getPaisOrigen());
assertEquals(nuevo.getFechaNacimiento(), guardado.getFechaNacimiento());
assertEquals(nuevo.getBiografia(), guardado.getBiografia());
assertEquals(nuevo.getGeneros(), guardado.getGeneros());
}

Tiene tres partes, que se repiten en todas las pruebas:

  1. Preparar: crearArtista() fabrica el artista que se va a crear.

  2. Ejecutar: se llama al método del Service.

  3. Verificar: primero que devolvió algo con id, y luego, con entityManager.find, que de verdad quedó guardado en la base y con cada uno de sus atributos, no solo el nombre. Si el Service olvidara copiar el país o la biografía, una prueba que solo mira el nombre pasaría igual. Verificar solo lo que devuelve el Service no basta: podría devolver un objeto bien armado y no haber guardado nada.

    flush y clear van antes del find: sin ellos, JPA te devuelve el mismo objeto que ya tiene en memoria y la comparación no pasaría por la base. Con ellos, el find lee de verdad lo que se escribió. Esos son los atributos de la ArtistaEntity del ejemplo (incluidos los generos): usa los de tu entidad, un assertEquals por cada uno, y agrega el de cada atributo nuevo.

Esta comparación supone que el Service guarda los valores tal cual. Si el tuyo los normaliza (por ejemplo, recorta los espacios del nombre), compara contra el valor ya normalizado y no contra el que enviaste.

Córrela:

Ventana de terminal
./mvnw test -Dtest=ArtistaServiceTest

Debes ver Tests run: 1, Failures: 0, Errors: 0. Si no compila, lo más probable es un import: revisa los de la sección 3.


Cada regla de tu tabla necesita una prueba en la que se viola. create tiene seis comportamientos que verificar además del caso feliz:

@Test
void testCreateArtistaSinNombre() {
ArtistaEntity nuevo = Instancio.of(ArtistaEntity.class)
.ignore(field(ArtistaEntity::getId))
.ignore(field(ArtistaEntity::getAlbumes))
.set(field(ArtistaEntity::getNombre), null)
.create();
assertThrows(IllegalOperationException.class,
() -> artistaService.create(nuevo));
}
@Test
void testCreateArtistaNombreRepetido() {
ArtistaEntity nuevo = crearArtista();
nuevo.setNombre(artistaList.get(0).getNombre());
assertThrows(IllegalOperationException.class,
() -> artistaService.create(nuevo));
}
@Test
void testCreateArtistaIgnoraElId() throws Exception {
ArtistaEntity nuevo = crearArtista();
Long idExistente = artistaList.get(0).getId();
nuevo.setId(idExistente);
ArtistaEntity resultado = artistaService.create(nuevo);
assertNotEquals(idExistente, resultado.getId());
assertEquals(artistaList.size() + 1, artistaService.findAll().size());
}
@Test
void testCreateArtistaMenorDeEdad() {
ArtistaEntity nuevo = crearArtista();
nuevo.setFechaNacimiento(LocalDate.now().minusYears(18).plusDays(1));
assertThrows(IllegalOperationException.class,
() -> artistaService.create(nuevo));
}
@Test
void testCreateArtistaCumpleLaMayoriaDeEdadHoy() throws Exception {
ArtistaEntity nuevo = crearArtista();
nuevo.setFechaNacimiento(LocalDate.now().minusYears(18));
ArtistaEntity resultado = artistaService.create(nuevo);
assertNotNull(resultado.getId());
}
@Test
void testCreateArtistaSinFechaDeNacimiento() throws Exception {
ArtistaEntity nuevo = crearArtista();
nuevo.setFechaNacimiento(null);
ArtistaEntity resultado = artistaService.create(nuevo);
assertNotNull(resultado.getId());
}
  • assertThrows(Clase, () -> ...) pasa solo si el bloque lanza esa excepción. Si el Service no la lanza, la prueba falla: ese es el caso en que la regla se perdió.
  • set(field(...), null) le dice a Instancio que el nombre es null. Es la forma de armar un dato inválido a propósito, sin tocar la entidad de producción. El resto de los atributos sigue siendo válido, así que lo único que puede hacer fallar al Service es el nombre.
  • El nombre repetido se arma con un dato que ya existe en la base: el nombre de artistaList.get(0).
  • testCreateArtistaIgnoraElId comprueba la decisión del tutorial del Service: si llega un id que ya existe, create no actualiza ese artista, crea uno nuevo. La prueba lo verifica con dos datos: el id devuelto es otro, y hay un artista más.

Las tres pruebas de la fecha rodean el límite de la regla. Quien cumple 18 años mañana todavía es menor (minusYears(18).plusDays(1)), y quien los cumple hoy ya es mayor (minusYears(18)): un solo día separa el error del caso válido. Es donde un isAfter mal escrito se equivoca, y una prueba con una fecha cualquiera no lo detectaría. La tercera comprueba que, sin fecha, la regla no se aplica.

Cada prueba debe tener un solo motivo para fallar. Por eso se nombran por el comportamiento que verifican (testCreateArtistaSinNombre) y no por el método (testCreate2).


Con el mismo patrón, agrega las pruebas de las otras operaciones. Como mínimo, dos pruebas por método: el caso feliz y el caso en que falla.

// ---------- findAll / findById
@Test
void testFindAll() {
assertEquals(artistaList.size(), artistaService.findAll().size());
}
@Test
void testFindAllVacio() {
clearData();
assertTrue(artistaService.findAll().isEmpty());
}
@Test
void testFindById() throws Exception {
ArtistaEntity esperado = artistaList.get(1);
ArtistaEntity resultado = artistaService.findById(esperado.getId());
assertEquals(esperado.getNombre(), resultado.getNombre());
}
@Test
void testFindByIdInexistente() {
assertThrows(EntityNotFoundException.class,
() -> artistaService.findById(0L));
}
// ---------- update
@Test
void testUpdateArtista() throws Exception {
ArtistaEntity existente = artistaList.get(0);
ArtistaEntity cambios = crearArtista();
artistaService.update(existente.getId(), cambios);
entityManager.flush();
entityManager.clear();
ArtistaEntity guardado =
entityManager.find(ArtistaEntity.class, existente.getId());
assertEquals(cambios.getNombre(), guardado.getNombre());
assertEquals(cambios.getPaisOrigen(), guardado.getPaisOrigen());
}
@Test
void testUpdateArtistaSinCambiarElNombre() throws Exception {
ArtistaEntity existente = artistaList.get(0);
ArtistaEntity cambios = crearArtista();
cambios.setNombre(existente.getNombre());
ArtistaEntity resultado =
artistaService.update(existente.getId(), cambios);
assertEquals(existente.getNombre(), resultado.getNombre());
}
@Test
void testUpdateArtistaConNombreDeOtro() {
ArtistaEntity existente = artistaList.get(0);
ArtistaEntity cambios = crearArtista();
cambios.setNombre(artistaList.get(1).getNombre());
assertThrows(IllegalOperationException.class,
() -> artistaService.update(existente.getId(), cambios));
}
@Test
void testUpdateArtistaMenorDeEdad() {
ArtistaEntity existente = artistaList.get(0);
ArtistaEntity cambios = crearArtista();
cambios.setFechaNacimiento(LocalDate.now().minusYears(10));
assertThrows(IllegalOperationException.class,
() -> artistaService.update(existente.getId(), cambios));
}
@Test
void testUpdateArtistaInexistente() {
ArtistaEntity cambios = crearArtista();
assertThrows(EntityNotFoundException.class,
() -> artistaService.update(0L, cambios));
}
// ---------- delete
@Test
void testDeleteArtista() throws Exception {
ArtistaEntity existente = artistaList.get(0);
artistaService.delete(existente.getId());
entityManager.flush();
entityManager.clear();
assertNull(entityManager.find(ArtistaEntity.class, existente.getId()));
}
@Test
void testDeleteArtistaInexistente() {
assertThrows(EntityNotFoundException.class,
() -> artistaService.delete(0L));
}

Hay detalles que conviene entender:

  • flush() y clear() fuerzan a que los cambios lleguen a la base y a que la siguiente búsqueda lea de la base, no de la memoria de la transacción. Sin ellos, find devolvería el mismo objeto que acabas de modificar y la prueba pasaría aunque nada se hubiera guardado.
  • testFindById busca el segundo artista (get(1)), no el primero. Si buscara el primero, un findById que devolviera siempre el primer registro pasaría la prueba sin buscar nada.
  • id 0L como inexistente: la base genera ids desde 1, así que 0 nunca existe.
  • Las pruebas de update cubren la regla de unicidad en sus dos lados. testUpdateArtistaSinCambiarElNombre verifica que un artista puede conservar su propio nombre, y testUpdateArtistaConNombreDeOtro que no puede tomar el de otro. Son las dos mitades del if del tutorial del Service.

El tutorial del Service advertía que update no debe guardar el objeto recibido, porque borraría los álbumes del artista. Esa advertencia merece su propia prueba:

@Test
void testUpdateArtistaConservaSusAlbumes() throws Exception {
AlbumEntity album = Instancio.of(AlbumEntity.class)
.ignore(field(AlbumEntity::getId))
.ignore(field(AlbumEntity::getArtistas))
.ignore(field(AlbumEntity::getCanciones))
.create();
entityManager.persist(album);
ArtistaEntity existente = artistaList.get(0);
existente.getAlbumes().add(album);
entityManager.flush();
artistaService.update(existente.getId(), crearArtista());
entityManager.flush();
entityManager.clear();
ArtistaEntity guardado =
entityManager.find(ArtistaEntity.class, existente.getId());
assertEquals(1, guardado.getAlbumes().size());
}

Se prepara un artista con un álbum (ignorando todas las relaciones del álbum, igual que en crearArtista()), se actualiza con datos que no traen álbumes, y se verifica que el álbum sigue ahí. Con ignore en los álbumes de crearArtista(), el objeto que recibe update no trae ninguno, y justo ese es el caso que rompería una implementación incorrecta.

Corre todo:

Ventana de terminal
./mvnw test -Dtest=ArtistaServiceTest

Debes ver Tests run: 19, Failures: 0, Errors: 0.


9. Comprobar que tus pruebas detectan errores

Sección titulada «9. Comprobar que tus pruebas detectan errores»

Que las pruebas pasen no demuestra que sean buenas. Una prueba que nunca falla no protege nada. Para saber si la tuya protege, rompe una regla a propósito y mira si alguna prueba se queja.

Haz estos experimentos, uno por uno, en una rama de práctica (ver el aviso al final de la sección) y restaura el Service después de cada uno:

Cambio en ArtistaService Prueba que debe fallar
En create, borrar la línea artista.setId(null); testCreateArtistaIgnoraElId
En create, hacer que la regla del nombre repetido nunca se cumpla (if (false) en lugar de if (!conElMismoNombre.isEmpty())) testCreateArtistaNombreRepetido
En create y update, borrar la llamada a validarNombre(artista); testCreateArtistaSinNombre
En update, cambiar if (!otro.getId().equals(id)) por if (true) testUpdateArtistaSinCambiarElNombre
En create y update, borrar la llamada a validarMayoriaDeEdad(artista); testCreateArtistaMenorDeEdad y testUpdateArtistaMenorDeEdad
En validarMayoriaDeEdad, cambiar nacimiento.isAfter( por !nacimiento.isBefore( testCreateArtistaCumpleLaMayoriaDeEdadHoy
En update, guardar el objeto recibido en lugar de existente testUpdateArtistaConservaSusAlbumes
En findById, devolver siempre el primer registro (return artistaRepository.findAll().get(0);) testFindById
En findAll, devolver una lista vacía (return new ArrayList<>();) testFindAll

Por ejemplo, con la primera, la salida dice algo así:

ArtistaServiceTest.testCreateArtistaIgnoraElId: expected: not equal but was: <32>
Tests run: 19, Failures: 1

Si al romper una regla ninguna prueba falla, falta una prueba: es la señal de que esa regla no está cubierta. Escríbela, vuelve a romper la regla y comprueba que ahora sí falla. Restaura el código y que vuelva a pasar.


Las pruebas que acabas de escribir ya se ejecutan en Jenkins: el Jenkinsfile del proyecto corre mvn test desde el Release 5 y publica los resultados de JUnit. No hay nada que modificar. Con tu siguiente push, el job las corre y muestra el resultado de cada una. Una prueba que falla deja el build en rojo.


  • @DataJpaTest o TestEntityManager no compilan. Falta la dependencia spring-boot-starter-data-jpa-test o los imports son de Spring Boot 3 (sección 3).
  • findAll devuelve más artistas de los esperados. El data.sql se está cargando en la base de pruebas. Falta properties = "spring.sql.init.mode=never" en @DataJpaTest.
  • Falla al persistir los datos de setUp. Se está generando un id o una relación. Revisa que crearArtista() ignore el id y todas las relaciones.
  • El Service no se encuentra (NoSuchBeanDefinitionException). Falta @Import(ArtistaService.class).
  • Mis pruebas de create fallan a veces, sin que haya cambiado el código. Dejaste aleatorio un atributo que tu Service valida (una fecha, una URL). Genéralo dentro de lo válido con generate o set en crearArtista() (sección 5).
  • La prueba pasa aunque el Service busque siempre el primer registro. Tu prueba usa artistaList.get(0). Busca otro elemento (get(1)): así un findById que devuelva siempre el primero falla.
  • La prueba pasa aunque el Service tenga un error. Verificas solo lo que devuelve el método. Consulta la base con entityManager.find (con flush y clear si el Service modifica algo).
  • Una prueba depende de otra. Si una prueba solo pasa después de otra, está usando datos que dejó la anterior. Cada prueba debe fabricar o buscar lo que necesita a partir de setUp.
  • Pruebas con nombres como test1. Cuando una falla en Jenkins, el nombre es lo único que ves. Que diga qué comportamiento verifica.

Hay una parte que sí se puede delegar: las pruebas análogas. Cuando ya escribiste a mano la primera prueba y tienes el setUp, una IA puede generar las de las demás operaciones y las de tus otras entidades, siguiendo el mismo patrón. Pásale tu ArtistaServiceTest como ejemplo y el Service que quieres probar.

Lo que no se delega:

  • Qué pruebas hay que escribir. Cada regla de tu tabla de la wiki necesita su caso de error. Es tu tabla, no la deduce la IA.
  • Revisar que cada prueba pruebe algo. Una IA puede escribir pruebas que siempre pasan. La defensa es el experimento de la sección 9: rompe la regla y comprueba que la prueba falla.
  • Los imports y las dependencias de Spring Boot 4. Un modelo entrenado con ejemplos de Spring Boot 3 sugiere paquetes que no existen en tu versión.

Al final de este tutorial tienes:

  • Las dependencias y los imports correctos de Spring Boot 4 para probar con JPA, y el data.sql fuera de tus pruebas.
  • Un ArtistaServiceTest con un setUp que deja la base en un estado conocido, y datos de prueba generados con Instancio.
  • Un caso feliz y un caso de error por cada método y cada regla del Service.
  • La comprobación de que las pruebas fallan cuando una regla se rompe.

Lo que viene después, siguiendo el orden de Release 7:

  • Repetir el patrón con el Service de cada entidad de tu módulo.
  • Cuando escribas el Service de asociaciones, un archivo de prueba por cada relación (ArtistaAlbumServiceTest).