Al finalizar este tutorial el estudiante estará en capacidad de realizar la implementación de la lógica junto con las pruebas de una entidad (y sus asociaciones) utilizando el framework Spring Boot.
Para realizar este tutorial Ud. debe:
El siguiente diagrama muestra el modelo conceptual del ejemplo. Basándonos en este diagrama, vamos a construir la capa de lógica (services) del concepto Book y sus asociaciones.
El siguiente diagrama muestra el diagrama de entidades, basado en el del modelo conceptual, donde se indican las distintas anotaciones que deben definirse en las asociaciones entre las clases:
La siguiente figura muestra las clases que implementan la persistencia para el concepto Book
y el concepto Author
. Tenemos un paquete que contiene las entidades y el paquete que contiene las interfaces Repository
, una por cada concepto. Estas interfaces extienden de una interface predefinida en String
llamada JpaRepository
. Esta interface declara todos los métodos (en el diagrama sólo están mostrados los que vamos a utilizar) que permiten interactuar con la base de datos para crear una entidad, obtener todas las entidades, obtener una entidad dada su llave primaria o borrar una entidad. Nosotros no definimos la implementación de estos métodos. Estas implementaciones las crea el ambiente de Spring.
.
Las clases que implementan la lógica están contenidas dentro de un paquete llamado services.
La responsabilidad de estas clases es validar las reglas de negocio e invocar a la persistencia.
En este diseño tenemos una clase de lógica (Service) por cada concepto pero también por cada asociación (una para sentido). Las clases services tienen los métodos para validar reglas de negocio y para invocar a la persistencia. El siguiente diagrama muestra un fragmento: Tenemos las clases AuthorService
y BookService
cuyas responsabilidades son traer todos los autores (o los libros), traer un autor (o un libro), crear un autor (o un libro) y borrar un autor o un libro respectivamente.
También tenemos las clases BookAuthorService
y AuthorBookService cuyas responsabilidades
son agregar a un libro un autor (o a un autor un libro), remplazar, borrar, etc.
En este tutorial usaremos la clase BookService
para ir explicando gradualmente cada uno de los métodos.
También explicaremos cómo probar los métodos. El siguiente diagrama incluye una clase de pruebas de la lógica:
Para que el framework Spring sepa que estamos definiendo una clase de lógica, esta clase debemos anotarla con @Service.
Las clases servicios dependen, porque utilizan, las clases que implementan la persistencia o repositories.
Para que estas clases de persistencia puedan ser utilizadas, declaramos atributos cuyo tipo son los definidos como Repository.
En este ejemplo, BookService
utilizará dentro de sus métodos los métodos definidos en BookRepository
(más exactamente, los heredados de JpaRepository
y los que define).
Por esta razón declaramos un atributo bookRepository.
En ejecución este atributo tendrá como valor un objeto de la clase que implementa BookRepository.
De la creación de estos objetos y de la asignación a estos atributos se ocupa el framework Spring.
Para que lo haga, anotamos la declaración del atributo con @Autowired.
Esta forma de obtener una instancia de una clase de forma externa (en este caso el framework le da el valor al atributo) sigue el patrón de diseño Inyección de dependencias.
|
Este método de la lógica recibe un libro, valida las reglas de negocio asociadas con el libro y si todo está correcto, salva un nuevo libro en la base de datos.
Recibe como parámetro el BookEntity
que se salvará en la base de datos. Para la creación de un libro se han definido varias reglas de negocio:
1) la editorial no puede ser nula;
2) la editorial debe ser válida (que ya esté registrada);
3) el ISBN debe ser válido;
4) que el ISBN no esté asociado a un libro previamente almacenado.
En caso de que alguna de estas reglas no se cumpla se lanza una excepción del tipo IllegalOperationException
. Si las reglas se cumplen, el libro persiste o salva en la base de datos.
@Transactional
public BookEntity createBook(BookEntity bookEntity) throws EntityNotFoundException, IllegalOperationException {
log.info("Inicia proceso de creación del libro");
if (bookEntity.getEditorial() == null)
throw new IllegalOperationException("Editorial is not valid");
Optional<EditorialEntity> editorialEntity = editorialRepository.findById(bookEntity.getEditorial().getId());
if (editorialEntity.isEmpty())
throw new IllegalOperationException("Editorial is not valid");
if (!validateISBN(bookEntity.getIsbn()))
throw new IllegalOperationException("ISBN is not valid");
if (!bookRepository.findByIsbn(bookEntity.getIsbn()).isEmpty())
throw new IllegalOperationException("ISBN already exists");
bookEntity.setEditorial(editorialEntity.get());
log.info("Termina proceso de creación del libro");
return bookRepository.save(bookEntity);
}
Note que en el código anterior, cuando se dispara una excepción, se crea con un mensaje claro de qué fue lo que sucedió. Esto es muy importante porque permitirá al usuario entender el problema.
Este método obtiene una lista de todos los libros. Usa el método findAll de la clase BookRepository
.
/**
* Devuelve todos los libros que hay en la base de datos.
*
* @return Lista de entidades de tipo libro.
*/
@Transactional
public List<BookEntity> getBooks() {
log.info("Inicia proceso de consultar todos los libros");
return bookRepository.findAll();
}
Este método obtiene un libro por el id. Se usa el método findById
del repositorio. Note que este método retorna un objeto de tipo Optional. Esto significa que el elemento puede tener un valor asignado o que puede contener un valor nulo. De esta forma estamos obligados a comprobar si la variable es null
antes de acceder a su valor.
En caso de que no exista un libro que tenga el id proporcionado se lanzará una excepción. En caso contrario, el método retorna el libro encontrado.
/**
* Busca un libro por ID
*
* @param bookId El id del libro a buscar
* @return El libro encontrado
* @throws EntityNotFoundException Si el libro no se encuentra
*/
@Transactional
public BookEntity getBook(Long bookId) throws EntityNotFoundException {
log.info("Inicia proceso de consultar el libro con id = {0}", bookId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
log.info("Termina proceso de consultar el libro con id = {0}", bookId);
return bookEntity.get();
}
Este método se encarga de actualizar un libro por el id. Antes de la actualización debemos comprobar que el libro exista en la base de datos o si no se lanzará una excepción. También se validan las reglas de negocio (semejante a lo que se hizo en el proceso de creación). Si los datos son correctos, el libro se actualiza en la base de datos.
/**
* Actualizar un libro por ID
*
* @param bookId El ID del libro a actualizar
* @param book La entidad del libro con los cambios deseados
* @return La entidad del libro luego de actualizarla
* @throws IllegalOperationException Si el ISBN de la actualización es inválido
* @throws EntityNotFoundException Si libro no es encontrado
*/
@Transactional
public BookEntity updateBook(Long bookId, BookEntity book)
throws EntityNotFoundException, IllegalOperationException {
log.info("Inicia proceso de actualizar el libro con id = {0}", bookId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
if (!validateISBN(book.getIsbn()))
throw new IllegalOperationException("ISBN is not valid");
book.setId(bookId);
log.info("Termina proceso de actualizar el libro con id = {0}", bookId);
return bookRepository.save(book);
}
Este método borra un libro por el id. En este método también se comprueba que el libro exista. Adicionalmente se valida que que el libro no tenga autores asociados, caso en el cual no se podría eliminar.
@Transactional
public void deleteBook(Long bookId) throws EntityNotFoundException, IllegalOperationException {
log.info("Inicia proceso de borrar el libro con id = {0}", bookId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
List<AuthorEntity> authors = bookEntity.get().getAuthors();
if (!authors.isEmpty())
throw new IllegalOperationException("Unable to delete book because it has associated authors");
bookRepository.deleteById(bookId);
log.info("Termina proceso de borrar el libro con id = {0}", bookId);
}
Las asociaciones de una entidad también deben ser incluidas en la capa de lógica. Para estos casos se crean generalmente clases aparte para modelar y probar las asociaciones por separado. A continuación nos centraremos en explicar la asociación Book-Author en la clase BookAuthorService.
Esta clase contiene la inyección de la capa de persistencia de ambas entidades, tanto la de Book como la de Author. Ahora revisaremos los métodos CRUD de la asociación junto a sus reglas de negocio.
@Slf4j
@Service
public class BookAuthorService {
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
...
}
Este método agrega un autor a un libro. Se valida que tanto autor como libro existan. Luego de la validación se realiza la asignación.
/**
* Asocia un Author existente a un Book
*
* @param bookId Identificador de la instancia de Book
* @param authorId Identificador de la instancia de Author
* @return Instancia de AuthorEntity que fue asociada a Book
*/
@Transactional
public AuthorEntity addAuthor(Long bookId, Long authorId) throws EntityNotFoundException {
log.info("Inicia proceso de asociarle un autor al libro con id = {0}", bookId);
Optional<AuthorEntity> authorEntity = authorRepository.findById(authorId);
if (authorEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
bookEntity.get().getAuthors().add(authorEntity.get());
log.info("Termina proceso de asociarle un autor al libro con id = {0}", bookId);
return authorEntity.get();
}
Este método obtiene la lista de autores de un libro en particular. Recibe como parámetro el id del libro a consultar.
/**
* Obtiene una colección de instancias de AuthorEntity asociadas a una instancia
* de Book
*
* @param bookId Identificador de la instancia de Book
* @return Colección de instancias de AuthorEntity asociadas a la instancia de
* Book
*/
@Transactional
public List<AuthorEntity> getAuthors(Long bookId) throws EntityNotFoundException {
log.info("Inicia proceso de consultar todos los autores del libro con id = {0}", bookId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
log.info("Finaliza proceso de consultar todos los autores del libro con id = {0}", bookId);
return bookEntity.get().getAuthors();
}
Este método obtiene un autor específico de un libro en particular.
/**
* Obtiene una instancia de AuthorEntity asociada a una instancia de Book
*
* @param bookId Identificador de la instancia de Book
* @param authorId Identificador de la instancia de Author
* @return La entidad del Autor asociada al libro
*/
@Transactional
public AuthorEntity getAuthor(Long bookId, Long authorId)
throws EntityNotFoundException, IllegalOperationException {
log.info("Inicia proceso de consultar un autor del libro con id = {0}", bookId);
Optional<AuthorEntity> authorEntity = authorRepository.findById(authorId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (authorEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
log.info("Termina proceso de consultar un autor del libro con id = {0}", bookId);
if (bookEntity.get().getAuthors().contains(authorEntity.get()))
return authorEntity.get();
throw new IllegalOperationException("The author is not associated to the book");
}
En este método se reemplazan los autores de un libro. Recibe como parámetro el id del libro a actualizar y el listado de los nuevos autores.
/**
* Remplaza las instancias de Author asociadas a una instancia de Book
*
* @param bookId Identificador de la instancia de Book
* @param list Colección de instancias de AuthorEntity a asociar a instancia
* de Book
* @return Nueva colección de AuthorEntity asociada a la instancia de Book
*/
@Transactional
public List<AuthorEntity> replaceAuthors(Long bookId, List<AuthorEntity> list) throws EntityNotFoundException {
log.info("Inicia proceso de reemplazar los autores del libro con id = {0}", bookId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
for (AuthorEntity author : list) {
Optional<AuthorEntity> authorEntity = authorRepository.findById(author.getId());
if (authorEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);
if (!bookEntity.get().getAuthors().contains(authorEntity.get()))
bookEntity.get().getAuthors().add(authorEntity.get());
}
log.info("Termina proceso de reemplazar los autores del libro con id = {0}", bookId);
return getAuthors(bookId);
}
Este método remueve un autor de un libro en particular.
/**
* Desasocia un Author existente de un Book existente
*
* @param bookId Identificador de la instancia de Book
* @param authorId Identificador de la instancia de Author
*/
@Transactional
public void removeAuthor(Long bookId, Long authorId) throws EntityNotFoundException {
log.info("Inicia proceso de borrar un autor del libro con id = {0}", bookId);
Optional<AuthorEntity> authorEntity = authorRepository.findById(authorId);
Optional<BookEntity> bookEntity = bookRepository.findById(bookId);
if (authorEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);
if (bookEntity.isEmpty())
throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);
bookEntity.get().getAuthors().remove(authorEntity.get());
log.info("Termina proceso de borrar un autor del libro con id = {0}", bookId);
}
En src/test/java, en el paquete services
encontramos los archivos de prueba de la lógica del proyecto. Todas las clases tienen el sufijo ServiceTest
que es la convención de nombramiento para las clases de prueba de la lógica.
Las clases de las pruebas tienen varias anotaciones:
@ExtendWith(SpringExtension.class)
. Esto indica que la clase de pruebas extiende de Spring@DataJpaTest
. Indica que en la prueba se involucra el acceso a datos@Transactional
. Indica que los métodos en la prueba serán transaccionales.@Import({{Nombre_Entidad}}Service.class)
. Indica la clase del servicio que se usará en el test. En cada clase se debe inyectar su servicio y el Entity Manager.
@Autowired
private {{Nombre_Entidad}}Service {{nombre_entidad}}Service;
@Autowired
private TestEntityManager entityManager;
La primera inyección de dependencias se usa para tener acceso a los métodos del servicio mientras que la segunda define un EntityManager
para las pruebas (acceso a métodos para persistir y recuperar datos de la persistencia).
Cada clase de prueba debe agregar una referencia a Podam, una librería que permite crear nuevos objetos con datos ficticios.
private PodamFactory factory = new PodamFactoryImpl();
El siguiente paso es la configuración inicial de la prueba. Esto se hace en el método setUp
anotado por @BeforeEach
, lo que indica que este método se ejecuta antes de cada test. En este método se hace el llamado a los métodos clearData()
e insertData()
. El primero borra los datos de las tablas implicadas en las pruebas y el segundo se encarga de insertar varios registros iniciales con ayuda del EntityManager
y se guardan en una lista previamente definida.
Para seguir entendiendo los métodos de la clase de pruebas, usaremos como ejemplo el contenido de la clase BookServiceTest
.
@ExtendWith(SpringExtension.class)
@DataJpaTest
@Transactional
@Import(BookService.class)
class BookServiceTest {
@Autowired
private BookService bookService;
@Autowired
private TestEntityManager entityManager;
private PodamFactory factory = new PodamFactoryImpl();
private List<BookEntity> bookList = new ArrayList<>();
private List<EditorialEntity> editorialList = new ArrayList<>();
/**
* Configuración inicial de la prueba.
*/
@BeforeEach
void setUp() {
clearData();
insertData();
}
/**
* Limpia las tablas que están implicadas en la prueba.
*/
private void clearData() {
entityManager.getEntityManager().createQuery("delete from BookEntity");
entityManager.getEntityManager().createQuery("delete from EditorialEntity");
entityManager.getEntityManager().createQuery("delete from AuthorEntity");
}
/**
* Inserta los datos iniciales para el correcto funcionamiento de las pruebas.
*/
private void insertData() {
for (int i = 0; i < 3; i++) {
EditorialEntity editorialEntity = factory.manufacturePojo(EditorialEntity.class);
entityManager.persist(editorialEntity);
editorialList.add(editorialEntity);
}
for (int i = 0; i < 3; i++) {
BookEntity bookEntity = factory.manufacturePojo(BookEntity.class);
bookEntity.setEditorial(editorialList.get(0));
entityManager.persist(bookEntity);
bookList.add(bookEntity);
}
AuthorEntity authorEntity = factory.manufacturePojo(AuthorEntity.class);
entityManager.persist(authorEntity);
authorEntity.getBooks().add(bookList.get(0));
bookList.get(0).getAuthors().add(authorEntity);
}
. . .
}
Como podemos observar, tenemos la inyección del servicio BookService
y del EntityManager
, al igual que tenemos la referencia a Podam y una lista de BookEntity
. El método setUp()
se llama antes de ejecutar cada método de prueba y este a su vez llama los métodos clearData()
e insertData()
. En el método clearData()
limpiamos todas las tablas implicadas en la prueba y aquellas relacionadas con la entidad BookEntity
, en este caso EditorialEntity
y AuthorEntity
. Luego de haber limpiado la base de datos el método insertData()
crea 3 EditorialEntity
y 3 BookEntity
arbitrarios usando la librería Podam. Note que también se crea un AuthorEntity. Esto se hace porque una de las reglas de negocio que se ha establecido es que no se puede eliminar un libro que tenga asociado un autor. Este autor se usa para asociarlo a un libro y verificar que se cumple esta regla de negocio al momento de eliminar un libro.
Recuerde que los atributos marcados con @PodamExclude
en la entidad no son autegenerados y siempre son inicializados en null. En el caso de la entidad BookEntity no se generan en Podam los atributos id, editorial, reviews y authors. El id no se genera por Podam ya que este es asignado por la persistencia cuando se guarda el objeto. La editorial tampoco se genera de forma automática dado que se requiere que sea una entidad que exista en la base de datos porque de lo contrario se lanzaría una excepción. Los atributos reviews y authors tampoco se generan de forma automática porque estos se prueban en las clases que prueban los servicios de las asociaciones, que para el caso de este ejemplo son AuthorBookService y BookAuthorService.
Este método prueba la creación de un libro. Se define una nueva entidad de tipo BookEntity
. Cada uno de sus atributos es seteado usando datos ficticios proporcionados por la factoría de Podam. Como debemos asegurarnos que los datos no van a violar las reglas de negocio definidas para la creación de un libro se ha seteado de forma manual la editorial (una editorial creada previamente en el setup de la prueba) y un ISBN que no es ni nulo ni una cadena vacía. Haciendo uso del servicio se crea el libro y se valida que este no haya sido null. Luego con ayuda del entity manager
se busca el libro creado y se compara con la entidad que sirvió de base para su creación; por tanto todos los atributos deben ser idénticos.
/**
* Prueba para crear un Book
*/
@Test
void testCreateBook() throws EntityNotFoundException, IllegalOperationException {
BookEntity newEntity = factory.manufacturePojo(BookEntity.class);
newEntity.setEditorial(editorialList.get(0));
newEntity.setIsbn("1-4028-9462-7");
BookEntity result = bookService.createBook(newEntity);
assertNotNull(result);
BookEntity entity = entityManager.find(BookEntity.class, result.getId());
assertEquals(newEntity.getId(), entity.getId());
assertEquals(newEntity.getName(), entity.getName());
assertEquals(newEntity.getDescription(), entity.getDescription());
assertEquals(newEntity.getImage(), entity.getImage());
assertEquals(newEntity.getPublishingDate(), entity.getPublishingDate());
assertEquals(newEntity.getIsbn(), entity.getIsbn());
}
En este test se espera que se lance una excepción dado que se está violando una regla de negocio. Como se observa, se está creando un libro y se le está agregando un ISBN que no es válido (una cadena vacía), por lo tanto el servicio deberá lanzar una excepción.
/**
* Prueba para crear un Book con ISBN inválido
*/
@Test
void testCreateBookWithNoValidISBN() {
assertThrows(IllegalOperationException.class, () -> {
BookEntity newEntity = factory.manufacturePojo(BookEntity.class);
newEntity.setEditorial(editorialList.get(0));
newEntity.setIsbn("");
bookService.createBook(newEntity);
});
}
En este método se crea un libro y se le asigna el ISBN de un libro previamente creado (almacenado en la posición 0 de la lista de libros). Se espera el lanzamiento de una excepción porque el ISBN es usado por otro libro.
/**
* Prueba para crear un Book con ISBN existente.
*/
@Test
void testCreateBookWithStoredISBN() {
assertThrows(IllegalOperationException.class, () -> {
BookEntity newEntity = factory.manufacturePojo(BookEntity.class);
newEntity.setEditorial(editorialList.get(0));
newEntity.setIsbn(bookList.get(0).getIsbn());
bookService.createBook(newEntity);
});
}
En este método se prueba el comportamiento del método createBook
del servicio cuando se asigna una editorial que no existe (la editorial con el id 0) a un nuevo libro.
/**
* Prueba para crear un Book con una editorial que no existe
*/
@Test
void testCreateBookWithInvalidEditorial() {
assertThrows(IllegalOperationException.class, () -> {
BookEntity newEntity = factory.manufacturePojo(BookEntity.class);
EditorialEntity editorialEntity = new EditorialEntity();
editorialEntity.setId(0L);
newEntity.setEditorial(editorialEntity);
bookService.createBook(newEntity);
});
}
En esta prueba se crea un libro y al atributo editorial se le asigna el valor null
; por tanto se espera el lanzamiento de una excepción.
/**
* Prueba para crear un Book con una editorial en null.
*/
@Test
void testCreateBookWithNullEditorial() {
assertThrows(IllegalOperationException.class, () -> {
BookEntity newEntity = factory.manufacturePojo(BookEntity.class);
newEntity.setEditorial(null);
bookService.createBook(newEntity);
});
}
En este método se prueba el servicio que se encarga de obtener todos los libros. Estos libros son almacenados en la variable list
. Como en la configuración inicial de la prueba se crearon tres libros que se almacenaron en la variable bookList
, se espera que los números de elementos de list
y de bookList
sean iguales. Luego se itera sobre la lista retornada por el servicio y se espera que cada elemento de la iteración esté presente en la lista bookList
.
/**
* Prueba para consultar la lista de Books.
*/
@Test
void testGetBooks() {
List<BookEntity> list = bookService.getBooks();
assertEquals(bookList.size(), list.size());
for (BookEntity entity : list) {
boolean found = false;
for (BookEntity storedEntity : bookList) {
if (entity.getId().equals(storedEntity.getId())) {
found = true;
}
}
assertTrue(found);
}
}
En este test se prueba el método del servicio que obtiene un libro. Primero, en la variable entity
se almacena el libro que está en la posición inicial de la lista bookList
. Luego se busca en la base de datos el libro que tiene ese id. Se espera entonces que ese libro exista en la base de datos. Luego, para completar la prueba se verifica que todos los atributos del elemento de la lista y del objeto retornado por el servicio sean iguales.
/**
* Prueba para consultar un Book.
*/
@Test
void testGetBook() throws EntityNotFoundException {
BookEntity entity = bookList.get(0);
BookEntity resultEntity = bookService.getBook(entity.getId());
assertNotNull(resultEntity);
assertEquals(entity.getId(), resultEntity.getId());
assertEquals(entity.getName(), resultEntity.getName());
assertEquals(entity.getDescription(), resultEntity.getDescription());
assertEquals(entity.getIsbn(), resultEntity.getIsbn());
assertEquals(entity.getImage(), resultEntity.getImage());
}
En esta prueba se valida que el servicio para obtener un libro lance una excepción cuando se busca un libro que no existe. En este caso se busca el libro con el id 0 el cual no existe. Acá es importante aclarar que los ids generados automáticamente por la base de datos siempre inician en 1, por tanto un id igual a 0 nunca va a existir.
/**
* Prueba para consultar un Book que no existe.
*/
@Test
void testGetInvalidBook() {
assertThrows(EntityNotFoundException.class,()->{
bookService.getBook(0L);
});
}
Este test valida el método del servicio que se encarga de actualizar un libro. Se inicia definiendo la variable entity
que almacena el primer libro de la lista bookList
. Luego, haciendo uso de Podam se crea un nuevo libro que se almacena en la variable pojoEntity
. A este objeto se le asigna en su atributo id el mismo id que tiene el libro almacenado en la variable entity
. Luego, se llama al método del servicio updateBook
que recibe como parámetro el id del libro que se quiere actualizar y un objeto con los nuevos valores para actualizar. Finalmente, usando el entity manager
se busca en la base de datos el libro que debió ser actualizado y se espera que los valores de todos los atributos hayan cambiado.
/**
* Prueba para actualizar un Book.
*/
@Test
void testUpdateBook() throws EntityNotFoundException, IllegalOperationException {
BookEntity entity = bookList.get(0);
BookEntity pojoEntity = factory.manufacturePojo(BookEntity.class);
pojoEntity.setId(entity.getId());
bookService.updateBook(entity.getId(), pojoEntity);
BookEntity resp = entityManager.find(BookEntity.class, entity.getId());
assertEquals(pojoEntity.getId(), resp.getId());
assertEquals(pojoEntity.getName(), resp.getName());
assertEquals(pojoEntity.getDescription(), resp.getDescription());
assertEquals(pojoEntity.getIsbn(), resp.getIsbn());
assertEquals(pojoEntity.getImage(), resp.getImage());
assertEquals(pojoEntity.getPublishingDate(), resp.getPublishingDate());
}
En este método se prueba que no se pueda actualizar un libro que no existe, en este caso el libro con el id 0. La prueba deberá lanzar una excepción indicando que la entidad no se ha encontrado.
/**
* Prueba para actualizar un Book inválido.
*/
@Test
void testUpdateBookInvalid() {
assertThrows(EntityNotFoundException.class, () -> {
BookEntity pojoEntity = factory.manufacturePojo(BookEntity.class);
pojoEntity.setId(0L);
bookService.updateBook(0L, pojoEntity);
});
}
En esta prueba se actualiza un libro al que previamente se le setea un ISBN inválido (en este caso una cadena vacía). El servicio deberá entonces lanzar una excepción que será validada por el test.
/**
* Prueba para actualizar un Book con ISBN inválido.
*/
@Test
void testUpdateBookWithNoValidISBN() {
assertThrows(IllegalOperationException.class, () -> {
BookEntity entity = bookList.get(0);
BookEntity pojoEntity = factory.manufacturePojo(BookEntity.class);
pojoEntity.setIsbn("");
pojoEntity.setId(entity.getId());
bookService.updateBook(entity.getId(), pojoEntity);
});
}
En esta prueba se llama al servicio que elimina un libro. A la variable entity se le asigna el primer libro de la lista bookList
. Luego se llama al método deleteBook
que recibe como parámetro el id del libro que se quiere eliminar. Posteriormente con ayuda del entity manager
se busca el libro con el id y se espera que, luego de haberse borrado, sea nulo.
/**
* Prueba para eliminar un Book.
*/
@Test
void testDeleteBook() throws EntityNotFoundException, IllegalOperationException {
BookEntity entity = bookList.get(1);
bookService.deleteBook(entity.getId());
BookEntity deleted = entityManager.find(BookEntity.class, entity.getId());
assertNull(deleted);
}
En este método se elimina un libro que no existe (el libro con el id 0). Al no existir el libro en la base de datos el servicio debe lanzar una excepción.
/**
* Prueba para eliminar un Book que no existe.
*/
@Test
void testDeleteInvalidBook() {
assertThrows(EntityNotFoundException.class, ()->{
bookService.deleteBook(0L);
});
}
Una de las reglas de negocio es que no se puede eliminar un libro que tenga asociado un autor. En la configuración inicial de la prueba al libro en la posición inicial de la lista bookList
se le agregó un autor. Por tanto, cuando se invoque al método deleteBook
con ese libro en especial, se debe lanzar una excepción.
/**
* Prueba para eliminar un Book con un author asociado.
*/
@Test
void testDeleteBookWithAuthor() {
assertThrows(IllegalOperationException.class, () -> {
BookEntity entity = bookList.get(0);
bookService.deleteBook(entity.getId());
});
}
La entidad BookEntity
tiene asociaciones con otras entidades. Una de ellas es con la entidad AuthorEntity
. Para manejar las asociaciones entre estas dos entidades se ha optado por crear un servicio adicional denominado BookAuthorService
. Las pruebas de los métodos de este servicio se han definido en la clase BookAuthorTestService
. Los siguientes son los detalles de las pruebas.
En la prueba se ha inyectado el servicio BookAuthorServicey
un entity manager
. También se incluye una referencia a Podam para generar datos de pruebas, un libro, una editorial y un listado de autores.
@Autowired
private BookAuthorService bookAuthorService;
@Autowired
private TestEntityManager entityManager;
private PodamFactory factory = new PodamFactoryImpl();
private BookEntity book = new BookEntity();
private EditorialEntity editorial = new EditorialEntity();
private List<AuthorEntity> authorList = new ArrayList<>();
En el setup se borran las tablas de la base de datos implicadas en la prueba en este caso AuthorEntity
y BookEntity
. Luego se crean los datos iniciales para cada prueba: una editorial, un nuevo libro al que se le asocia esa editorial, y un listado de autores que se le asignan al libro.
En este test se prueba el método que asocia un autor a un libro. La prueba inicia con la creación de un nuevo libro que se almacena en la variable newBook
. Este libro se persiste en la base de datos con ayuda del entity manager
. Luego, siguiendo el mismo esquema se crea un nuevo autor y se persiste. En el siguiente paso se llama al método addAuthorService
el cual recibe el id del libro y el id del autor. Posteriormente se obtiene el autor asignado al libro y todos los atributos deben corresponder con los almacenados en la variable author
.
/**
* Prueba para asociar un autor a un libro.
*
*/
@Test
void testAddAuthor() throws EntityNotFoundException, IllegalOperationException {
BookEntity newBook = factory.manufacturePojo(BookEntity.class);
newBook.setEditorial(editorial);
entityManager.persist(newBook);
AuthorEntity author = factory.manufacturePojo(AuthorEntity.class);
entityManager.persist(author);
bookAuthorService.addAuthor(newBook.getId(), author.getId());
AuthorEntity lastAuthor = bookAuthorService.getAuthor(newBook.getId(), author.getId());
assertEquals(author.getId(), lastAuthor.getId());
assertEquals(author.getBirthDate(), lastAuthor.getBirthDate());
assertEquals(author.getDescription(), lastAuthor.getDescription());
assertEquals(author.getImage(), lastAuthor.getImage());
assertEquals(author.getName(), lastAuthor.getName());
}
En esta prueba se le agrega a un libro un autor que no existe. Se inicia con la creación de un libro el cual se persiste con ayuda del entity manager
. Posteriormente se llama al método addAuthor
que recibe el id del libro creado y el id de un autor que no existe (el autor con el id 0); por tanto el servicio debe lanzar una excepción.
/**
* Prueba para asociar un autor que no existe a un libro.
*
*/
@Test
void testAddInvalidAuthor() {
assertThrows(EntityNotFoundException.class, ()->{
BookEntity newBook = factory.manufacturePojo(BookEntity.class);
newBook.setEditorial(editorial);
entityManager.persist(newBook);
bookAuthorService.addAuthor(newBook.getId(), 0L);
});
}
En este caso de prueba se asocia un autor a un libro que no existe. Se crea el autor, se persiste, y luego se llama al método de la lógica addAuthor
el cual recibe el id de un libro que no existe (el libro con el id 0) y el autor creado. Se espera entonces que el método del servicio lance una excepción.
/**
* Prueba para asociar un autor a un libro que no existe.
*
*/
@Test
void testAddAuthorInvalidBook() throws EntityNotFoundException, IllegalOperationException {
assertThrows(EntityNotFoundException.class, ()->{
AuthorEntity author = factory.manufacturePojo(AuthorEntity.class);
entityManager.persist(author);
bookAuthorService.addAuthor(0L, author.getId());
});
}
En este test se prueba el método que obtiene la lista de autores de un libro. En la configuración de la prueba se creó una lista de autores (authorList
). También se creó una variable denominada book
a la que se por medio del entity manager se le asignó esa lista de autores. En la prueba se obtiene la lista de autores de la entidad book (authorEntities
) y se espera que el tamaño de esa lista sea igual al tamaño de la lista authorList
. Luego se itera la lista autorList
y se espera que contenga los elementos de la lista authorEntities
.
/**
* Prueba para consultar la lista de autores de un libro.
*/
@Test
void testGetAuthors() throws EntityNotFoundException {
List<AuthorEntity> authorEntities = bookAuthorService.getAuthors(book.getId());
assertEquals(authorList.size(), authorEntities.size());
for (int i = 0; i < authorList.size(); i++) {
assertTrue(authorEntities.contains(authorList.get(0)));
}
}
En esta prueba se consultan los autores de un libro que no existe, por tanto se espera el lanzamiento de una excepción por parte del método getAuthors
al que se le pasa como parámetro el libro con el id 0.
/**
* Prueba para consultar la lista de autores de un libro que no existe.
*/
@Test
void testGetAuthorsInvalidBook(){
assertThrows(EntityNotFoundException.class, ()->{
bookAuthorService.getAuthors(0L);
});
}
En este método se prueba la consulta del autor de un libro. A la variable authorEntity
se le asigna el autor que está en la primera posición de la lista authorList
. Luego se espera que en en el objeto book exista el autor almacenado en authorEntity
.
/**
* Prueba para consultar un autor de un libro.
*
* @throws throws EntityNotFoundException, IllegalOperationException
*/
@Test
void testGetAuthor() throws EntityNotFoundException, IllegalOperationException {
AuthorEntity authorEntity = authorList.get(0);
AuthorEntity author = bookAuthorService.getAuthor(book.getId(), authorEntity.getId());
assertNotNull(author);
assertEquals(authorEntity.getId(), author.getId());
assertEquals(authorEntity.getName(), author.getName());
assertEquals(authorEntity.getDescription(), author.getDescription());
assertEquals(authorEntity.getImage(), author.getImage());
assertEquals(authorEntity.getBirthDate(), author.getBirthDate());
}
En este test se consulta un autor que no existe (el autor con el id 0) en el libro almacenado en la variable book
. Como el autor no existe el método del servicio deberá lanzar una excepción.
/**
* Prueba para consultar un autor que no existe de un libro.
*
* @throws throws EntityNotFoundException, IllegalOperationException
*/
@Test
void testGetInvalidAuthor() {
assertThrows(EntityNotFoundException.class, ()->{
bookAuthorService.getAuthor(book.getId(), 0L);
});
}
En este test se consulta el autor de un libro que no existe (el libro con el id 0). El método al no encontrar ese libro lanzará una excepción.
/**
* Prueba para consultar un autor de un libro que no existe.
*
* @throws throws EntityNotFoundException, IllegalOperationException
*/
@Test
void testGetAuthorInvalidBook() {
assertThrows(EntityNotFoundException.class, ()->{
AuthorEntity authorEntity = authorList.get(0);
bookAuthorService.getAuthor(0L, authorEntity.getId());
});
}
En esta prueba se busca obtener un autor que no está asociado a un libro. Se inicia con la creación de un nuevo libro que se almacena en la variable newBook
. Luego se crea un nuevo autor que se almacena en la variable author
. Nótese que autor y libro no han sido asociados de ninguna forma. Por tanto al llamar al método getAuthor
pasando como parámetros el id del nuevo libro y el id del nuevo autor se deberá lanzar una excepción dado que ese libro y ese autor no están relacionados.
/**
* Prueba para obtener un autor no asociado a un libro.
*
*/
@Test
void testGetNotAssociatedAuthor() {
assertThrows(IllegalOperationException.class, ()->{
BookEntity newBook = factory.manufacturePojo(BookEntity.class);
newBook.setEditorial(editorial);
entityManager.persist(newBook);
AuthorEntity author = factory.manufacturePojo(AuthorEntity.class);
entityManager.persist(author);
bookAuthorService.getAuthor(newBook.getId(), author.getId());
});
}
En este método se prueba la actualización de los autores de un libro. Se inicia con la creación de una lista de autores que se almacena en la variable nuevaLista
. Luego se reemplazan los autores de la entidad book
con los autores de la nueva lista. En el siguiente paso se obtienen los nuevos autores de book y se espera que sean los mismos que están almacenados en la variable nuevaLista
.
/**
* Prueba para actualizar los autores de un libro.
*
* @throws EntityNotFoundException
*/
@Test
void testReplaceAuthors() throws EntityNotFoundException {
List<AuthorEntity> nuevaLista = new ArrayList<>();
for (int i = 0; i < 3; i++) {
AuthorEntity entity = factory.manufacturePojo(AuthorEntity.class);
entityManager.persist(entity);
book.getAuthors().add(entity);
nuevaLista.add(entity);
}
bookAuthorService.replaceAuthors(book.getId(), nuevaLista);
List<AuthorEntity> authorEntities = bookAuthorService.getAuthors(book.getId());
for (AuthorEntity aNuevaLista : nuevaLista) {
assertTrue(authorEntities.contains(aNuevaLista));
}
}
En este test se prueba la actualización de los autores de un libro que no existe. Se sigue el mismo esquema de la prueba anterior con la creación de una nueva lista de autores. Luego se llama al método replaceAuthors
pasando el libro con el id 0 y la nueva lista. El método debe lanzar una excepción dado que el libro con el id 0 no existe.
/**
* Prueba para actualizar los autores de un libro que no existe.
*
* @throws EntityNotFoundException
*/
@Test
void testReplaceAuthorsInvalidBook(){
assertThrows(EntityNotFoundException.class, ()->{
List<AuthorEntity> nuevaLista = new ArrayList<>();
for (int i = 0; i < 3; i++) {
AuthorEntity entity = factory.manufacturePojo(AuthorEntity.class);
entity.getBooks().add(book);
entityManager.persist(entity);
nuevaLista.add(entity);
}
bookAuthorService.replaceAuthors(0L, nuevaLista);
});
}
En esta prueba se busca actualizar los autores que no existen de un libro. Se crea una lista de autores que inicialmente está vacía. A esa lista se agrega un nuevo autor con el id 0 (el cual no existe en la base de datos). Finalmente se llama al método replaceAuthors
el cual debe lanzar una excepción dado que la lista contiene un autor inexistente.
/**
* Prueba para actualizar los autores que no existen de un libro.
*
* @throws EntityNotFoundException
*/
@Test
void testReplaceInvalidAuthors() {
assertThrows(EntityNotFoundException.class, ()->{
List<AuthorEntity> nuevaLista = new ArrayList<>();
AuthorEntity entity = factory.manufacturePojo(AuthorEntity.class);
entity.setId(0L);
nuevaLista.add(entity);
bookAuthorService.replaceAuthors(book.getId(), nuevaLista);
});
}
En esta prueba se actualiza un autor de un libro que no existe. Se crea una lista de autores. Luego se llama al método replaceAuthors
pasando como parámetros el libro con el id 0 y la nueva lista creada. El método deberá lanzar una excepción dado que el libro con el id 0 no existe.
/**
* Prueba para actualizar un autor de un libro que no existe.
*
* @throws EntityNotFoundException
*/
@Test
void testReplaceAuthorsInvalidAuthor(){
assertThrows(EntityNotFoundException.class, ()->{
List<AuthorEntity> nuevaLista = new ArrayList<>();
for (int i = 0; i < 3; i++) {
AuthorEntity entity = factory.manufacturePojo(AuthorEntity.class);
entity.getBooks().add(book);
entityManager.persist(entity);
nuevaLista.add(entity);
}
bookAuthorService.replaceAuthors(0L, nuevaLista);
});
}
En esta prueba se desasocia un autor y un libro. Se toma la lista authorList
y se itera. Al objeto book
se elimina cada autor de esa lista. Luego se espera que cuando se obtenga el listado de los autores de ese libro, ese listado debe estar vacío.
/**
* Prueba desasociar un autor con un libro.
*
*/
@Test
void testRemoveAuthor() throws EntityNotFoundException {
for (AuthorEntity author : authorList) {
bookAuthorService.removeAuthor(book.getId(), author.getId());
}
assertTrue(bookAuthorService.getAuthors(book.getId()).isEmpty());
}
En este test se elimina un autor que no existe en libro. Se llama al método removeAuthor
con un libro y el autor con el id 0. Se espera una excepción en el método porque el autor con el id 0 no existe.
/**
* Prueba desasociar un autor que no existe con un libro.
*
*/
@Test
void testRemoveInvalidAuthor(){
assertThrows(EntityNotFoundException.class, ()->{
bookAuthorService.removeAuthor(book.getId(), 0L);
});
}
En este test se desasocia un autor con un libro que no existe. Al no existir el libro con el id 0 se espera que el método removeAuthor
lance una excepción.
/**
* Prueba desasociar un autor con un libro que no existe.
*
*/
@Test
void testRemoveAuthorInvalidBook(){
assertThrows(EntityNotFoundException.class, ()->{
bookAuthorService.removeAuthor(0L, authorList.get(0).getId());
});
}