Tutorial: Pruebas automáticas del Service con la base de datos
1. Objetivos de aprendizaje
Sección titulada «1. Objetivos de aprendizaje»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>ServiceTestcon@DataJpaTest,TestEntityManagery un@BeforeEachque 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.
2. De dónde partes
Sección titulada «2. De dónde partes»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:
ArtistaServiceTestprueba los métodos deArtistaService. Las relaciones entre entidades tendrán su propio archivo (por ejemploArtistaAlbumServiceTest) 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.
3. Preparar el proyecto
Sección titulada «3. Preparar el proyecto»Las dependencias
Sección titulada «Las dependencias»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-testtrae@DataJpaTestyTestEntityManager. Desde Spring Boot 4 no vienen con el starter de pruebas general: sin esta dependencia, esas clases no existen.instancio-junites la librería que fabrica los datos de prueba.
El data.sql y las pruebas
Sección titulada «El data.sql y las pruebas»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.
4. La estructura de la clase de prueba
Sección titulada «4. La estructura de la clase de prueba»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:
@DataJpaTestarranca solo la parte de JPA, sin servidor web, con una base H2 en memoria. Conpropertiesse desactiva eldata.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.@DataJpaTestno lo carga por sí solo, porque solo registra la capa de persistencia.TestEntityManagersirve 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ó.artistaListguarda los artistas que se insertan antes de cada prueba, para que las pruebas puedan usarlos.- Los dos imports de
exceptionson las excepciones del Service, que viven en el paquete compartidoco.edu.uniandes.musical.exception. Si en tu proyecto el paquete se llama distinto (por ejemploexceptions), usa el tuyo. Las pruebas de las secciones siguientes las usan conassertThrows.
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:
@BeforeEachvoid 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();}setUpse 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ñanaArtistaEntitygana un atributo, ninguna prueba cambia.ignore(field(...))deja un atributo sin valor. Elidse ignora porque lo asigna la base de datos, y losalbumesporque 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
createde 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).
6. Primera prueba: create (escríbela tú)
Sección titulada «6. Primera prueba: create (escríbela tú)»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.
@Testvoid 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:
-
Preparar:
crearArtista()fabrica el artista que se va a crear. -
Ejecutar: se llama al método del Service.
-
Verificar: primero que devolvió algo con
id, y luego, conentityManager.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.flushyclearvan antes delfind: 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, elfindlee de verdad lo que se escribió. Esos son los atributos de laArtistaEntitydel ejemplo (incluidos losgeneros): usa los de tu entidad, unassertEqualspor 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:
./mvnw test -Dtest=ArtistaServiceTestDebes 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.
7. Los casos de error: una prueba por regla
Sección titulada «7. Los casos de error: una prueba por regla»Cada regla de tu tabla necesita una prueba en la que se viola. create tiene
seis comportamientos que verificar además del caso feliz:
@Testvoid 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));}
@Testvoid testCreateArtistaNombreRepetido() { ArtistaEntity nuevo = crearArtista(); nuevo.setNombre(artistaList.get(0).getNombre());
assertThrows(IllegalOperationException.class, () -> artistaService.create(nuevo));}
@Testvoid 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());}
@Testvoid testCreateArtistaMenorDeEdad() { ArtistaEntity nuevo = crearArtista(); nuevo.setFechaNacimiento(LocalDate.now().minusYears(18).plusDays(1));
assertThrows(IllegalOperationException.class, () -> artistaService.create(nuevo));}
@Testvoid testCreateArtistaCumpleLaMayoriaDeEdadHoy() throws Exception { ArtistaEntity nuevo = crearArtista(); nuevo.setFechaNacimiento(LocalDate.now().minusYears(18));
ArtistaEntity resultado = artistaService.create(nuevo); assertNotNull(resultado.getId());}
@Testvoid 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 esnull. 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). testCreateArtistaIgnoraElIdcomprueba la decisión del tutorial del Service: si llega unidque ya existe,createno actualiza ese artista, crea uno nuevo. La prueba lo verifica con dos datos: eliddevuelto 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).
8. El resto de las operaciones
Sección titulada «8. El resto de las operaciones»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
@Testvoid testFindAll() { assertEquals(artistaList.size(), artistaService.findAll().size());}
@Testvoid testFindAllVacio() { clearData(); assertTrue(artistaService.findAll().isEmpty());}
@Testvoid testFindById() throws Exception { ArtistaEntity esperado = artistaList.get(1);
ArtistaEntity resultado = artistaService.findById(esperado.getId()); assertEquals(esperado.getNombre(), resultado.getNombre());}
@Testvoid testFindByIdInexistente() { assertThrows(EntityNotFoundException.class, () -> artistaService.findById(0L));}// ---------- update
@Testvoid 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());}
@Testvoid 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());}
@Testvoid testUpdateArtistaConNombreDeOtro() { ArtistaEntity existente = artistaList.get(0); ArtistaEntity cambios = crearArtista(); cambios.setNombre(artistaList.get(1).getNombre());
assertThrows(IllegalOperationException.class, () -> artistaService.update(existente.getId(), cambios));}
@Testvoid testUpdateArtistaMenorDeEdad() { ArtistaEntity existente = artistaList.get(0); ArtistaEntity cambios = crearArtista(); cambios.setFechaNacimiento(LocalDate.now().minusYears(10));
assertThrows(IllegalOperationException.class, () -> artistaService.update(existente.getId(), cambios));}
@Testvoid testUpdateArtistaInexistente() { ArtistaEntity cambios = crearArtista();
assertThrows(EntityNotFoundException.class, () -> artistaService.update(0L, cambios));}// ---------- delete
@Testvoid testDeleteArtista() throws Exception { ArtistaEntity existente = artistaList.get(0);
artistaService.delete(existente.getId()); entityManager.flush(); entityManager.clear();
assertNull(entityManager.find(ArtistaEntity.class, existente.getId()));}
@Testvoid testDeleteArtistaInexistente() { assertThrows(EntityNotFoundException.class, () -> artistaService.delete(0L));}Hay detalles que conviene entender:
flush()yclear()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,finddevolvería el mismo objeto que acabas de modificar y la prueba pasaría aunque nada se hubiera guardado.testFindByIdbusca el segundo artista (get(1)), no el primero. Si buscara el primero, unfindByIdque devolviera siempre el primer registro pasaría la prueba sin buscar nada.id0Lcomo inexistente: la base genera ids desde 1, así que0nunca existe.- Las pruebas de
updatecubren la regla de unicidad en sus dos lados.testUpdateArtistaSinCambiarElNombreverifica que un artista puede conservar su propio nombre, ytestUpdateArtistaConNombreDeOtroque no puede tomar el de otro. Son las dos mitades delifdel tutorial del Service.
Una prueba para lo que no debe cambiar
Sección titulada «Una prueba para lo que no debe cambiar»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:
@Testvoid 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:
./mvnw test -Dtest=ArtistaServiceTestDebes 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: 1Si 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.
10. El pipeline
Sección titulada «10. El pipeline»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.
11. Errores frecuentes
Sección titulada «11. Errores frecuentes»@DataJpaTestoTestEntityManagerno compilan. Falta la dependenciaspring-boot-starter-data-jpa-testo los imports son de Spring Boot 3 (sección 3).findAlldevuelve más artistas de los esperados. Eldata.sqlse está cargando en la base de pruebas. Faltaproperties = "spring.sql.init.mode=never"en@DataJpaTest.- Falla al persistir los datos de
setUp. Se está generando unido una relación. Revisa quecrearArtista()ignore elidy todas las relaciones. - El Service no se encuentra (
NoSuchBeanDefinitionException). Falta@Import(ArtistaService.class). - Mis pruebas de
createfallan 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 congenerateosetencrearArtista()(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í unfindByIdque 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(conflushyclearsi 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.
12. Con ayuda de la IA
Sección titulada «12. Con ayuda de la IA»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.
¿Dónde quedaste?
Sección titulada «¿Dónde quedaste?»Al final de este tutorial tienes:
- Las dependencias y los imports correctos de Spring Boot 4 para probar con
JPA, y el
data.sqlfuera de tus pruebas. - Un
ArtistaServiceTestcon unsetUpque 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
Servicede cada entidad de tu módulo. - Cuando escribas el Service de asociaciones, un archivo de prueba por cada
relación (
ArtistaAlbumServiceTest).