¿Qué aprenderá?

Al finalizar este tutorial el estudiante estará en capacidad de realizar la implementación de la persistencia y la lógica junto con las pruebas de una entidad, utilizando JPA y el framework Spring Boot.

¿Qué necesita?

Para realizar este tutorial Ud. debe:

  1. Tener el ambiente de desarrollo instalado. Para esto ud puede:
  1. Aprovisionar su máquina virtual con la configuración ya creada
  2. Instalar en su propia máquina el ambiente de desarrollo instalado. Si aún no lo tiene instalado puede seguir los pasos acá.
  1. Contar con el modelo de entidades
  2. Tener clonado el proyecto back, es decir haber llevado a cabo el tutorial de Ejecución-Back.

Siguiendo el diagrama de clases que se muestra a continuación, vamos a construir progresivamente una aplicación que permita, a través de servicios Restful, crear, modificar, borrar, obtener los recursos Book, Author, Editorial, Reviews, Prize y Organization teniendo en cuenta las relaciones entre estos elementos.

El siguiente diagrama muestra el modelo conceptual del ejemplo:

El ejemplo ha sido creado para que tenga distintos tipos de relaciones entre los recursos. La siguiente tabla describe las relaciones. En paréntesis está el nombre del extremo de la relación que se convertirá en el atributo que se modela en Java. Por ejemplo, la clase Book tendrá un atributo llamado reviews.

Asociación

Descripción

Book - Review

Un libro tiene un conjunto de revisiones (reviews)

Una revisión le pertenece a un único libro.

Book - Author

Un libro puede tener varios autores (al menos 1).(authors)

Un autor puede haber escrito varios libros (al menos uno) (books)

Book - Editorial

Un libro es editado por una única editorial (editorial).

Una editorial puede edita muchos libros (al menos 1) (books)

Author - Prize

Un autor puede haber recibido muchos premios o ninguno (prizes)

Un premio le pertenece a un único autor.

Prize - Organization

Un premio es otorgado por una única organización (ornanization)

Una organización otorga un único premio (prize)

Gradualmente vamos a ir viendo cómo estas relaciones se manejan desde la persistencia hasta la capa de servicios.

El siguiente diagrama muestra la transformación del modelo conceptual al modelo de entidades del ejemplo de BookStore. Las decisiones que se toman son las siguientes:

  1. Por cada clase del modelo conceptual se crea una clase que representa la entidad que va a persistir en la base de datos. Todas las entidades heredan de BaseEntity (no se incluye esto en el modelo por claridad).

  1. Para cada clase entidad, se toman todos los extremos opuestos de las relaciones que tiene con otras clases (clases destino de la relación). Cada extremo se anota de acuerdo con la cardinalidad de la asociación.
  2. Dependiendo del caso, en cada anotación también se debe incluir, entre otros:
  1. El atributo correspondiente en la clase destino
  2. El modo de cargue, es decir, si se hace en forma inmediata (EAGER) o en forma perezosa (LAZY)
  3. Si la operación de persistencia se va a propagar a las clases relacionadas con esta (Cascade).

Vamos a explicar cada una de las anotaciones en el modelo de entidades. Empezamos con la clase BookEntity que tiene tres asociaciones, con: Review, Author y Editorial

Cuando la clase BookEntity se escriba en Java, además de tener sus atributos básicos, tendrá un atributo por cada asociación en la que BookEntity es fuente. Los extremos opuestos de la asociación (del lado de la clase destino) tiene el nombre que se usará para modelar el atributo: reviews, editorial y authors. Además de tener un tipo, cada uno de estos atributos debe tener una anotación que le permita a JPA crear adecuadamente las tablas.

Esta es una relación de uno a muchos (OneToMany).

En java, la clase BookEntity tendrá la siguiente definición del atributo reviews:

...
@OneToMany(mappedBy = "book", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<ReviewEntity> reviews = new ArrayList<>();

...

En java, la clase ReviewEntity tendrá la siguiente definición del atributo book:

...
@ManyToOne
 private BookEntity book;

...

Note que la anotación @OneToMany sobre reviews tiene varios atributos:

  1. mappedBy = "book" para indicar que esta relación que estamos definiendo se refiere a la misma que en la clase ReviewEntity está definida con el atributo "book".
  2. Con respecto a las tablas significa que es la tabla de reviews quien tiene la llave foránea al libro que corresponde.
  3. cascade = CascadeType.PERSIST significa que cuando se persista un libro debe persistir cada una de sus reviews. En particular si un libro se borra se deben borrar todos sus reviews.
  4. orphanRemoval = true significa que no puede haber un reviews si no está asociado con un libro. No puede estar "huérfano".

Esta es una relación de muchos a muchos (ManyToMany).

En java, la clase BookEntity tendrá la siguiente definición del atributo authors:

...
@ManyToMany
private List<AuthorEntity> authors = new ArrayList<>();


...

En java, la clase AuthorEntity tendrá la siguiente definición del atributo books:

...
@ManyToMany(mappedBy = "authors")
private List<BookEntity> books = new ArrayList<>();


...

Note que en el extremo de authors se está definiendo el atributo que corresponde en el otro extremo (books).

Esta es una relación de muchos a uno (ManyToOne).

En java, la clase BookEntity tendrá la siguiente definición del atributo editorial:

..
@ManyToOne
private EditorialEntity editorial;
..

En java, la clase EditorialEntity tendrá la siguiente definición del atributo book:

..
@OneToMany(mappedBy = "editorial")
private List<BookEntity> books = new ArrayList<>();

..

Note que en el extremo de books se está definiendo el atributo que corresponde en el otro extremo (editorial).

El siguiente diagrama muestra la asociación que va de AuthorEntity a PrizeEntity y de ésta a OrganizationEntity.

En java, la clase AuthorEntity tendrá la siguiente definición del atributo books:

...
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<PrizeEntity> prizes = new ArrayList<>();
...

En java, la clase PrizeEntity tendrá la siguiente definición del atributo author:

...
        @ManyToOne
        private AuthorEntity author;

...

En java, la clase PrizeEntity tendrá la siguiente definición del atributo organization:

...
        @OneToOne
        private OrganizationEntity organization;

...

BaseEntity

La clase BaseEntity es una clase abstracta que es una superclase de todas las clases entidades del proyecto. Como podemos observar, esta contiene los imports de las anotaciones que usamos. En primer lugar @Data de la librería lombok nos sirve para autogenerar setters y getters de los atributos de la clase. Luego nos encontramos con la anotación @MappedSuperclass, que nos indica que esta es la clase de la cual las entidades van a heredar, además de omitir la creación de una tabla en la base de datos con esta clase.

Esta clase contiene el código que define el atributo id de tipo Long que corresponde a la llave primaria. Este atributo contiene varias anotaciones:

package co.edu.uniandes.dse.bookstore.entities;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import lombok.Data;
import uk.co.jemos.podam.common.PodamExclude;

/**
 * Entidad genérica de la que heredan todas las entidades. Contiene la
 * referencia al atributo id
 *
 * @author ISIS2603
 */

@Data
@MappedSuperclass
public abstract class BaseEntity {

        @PodamExclude
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
}

En los pasos anteriores revisamos las anotaciones relacionadas con las asociaciones entre las clases. Ahora vamos a revisar otras consideraciones y anotaciones para ser tenidas en cuenta cuando se define una entidad.

Atributos simples

Atributos tipo DATE

Cuando tenemos un atributo cuyo tipo de dato es un Date de Java, debemos anotarlo con

@Temporal(TemporalType.DATE). A través de esta anotación le asignamos a este campo una conexión entre el formato Date de Java y el formato de Fecha que maneja la base de datos. También tenemos sobre los atributos tipo DATE la anotación @PodamStrategyValue(DateStrategy.class). A través de esta anotación le estamos informando a la librería Podam que use la estrategia DateStrategy para generar datos de este atributos de una forma específica, por ejemplo, día/mes/año u otro formato. Dentro de la clase DateStrategy podemos ver la lógica del autogenerado de una fecha para más información. Esta información en detalle de cómo crear estrategias para autogenerado de valores la podemos encontrar en la documentación de Podam.

Anotaciones para la creación de datos con Podam

En los atributos que representan una asociación, utilizamos la anotación @PodamExclude para indicarle a la librería Podam que excluya este atributo a la hora de hacer la generación aleatoria de valores de estos atributos en las pruebas.

Ahora miremos la implementación de una entidad para este proyecto. Empecemos viendo la clase AuthorEntity. En esta encontraremos lo siguiente:

ackage co.edu.uniandes.dse.bookstore.entities;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import co.edu.uniandes.dse.bookstore.podam.DateStrategy;
import lombok.Getter;
import lombok.Setter;
import uk.co.jemos.podam.common.PodamExclude;
import uk.co.jemos.podam.common.PodamStrategyValue;

/**
 * Clase que representa un autor en la persistencia
 *
 * @author ISIS2603
 */

@Getter
@Setter
@Entity
public class AuthorEntity extends BaseEntity {

        @Temporal(TemporalType.DATE)
        @PodamStrategyValue(DateStrategy.class)
        private Date birthDate;

        @PodamExclude
        @ManyToMany(mappedBy = "authors")
        private List<BookEntity> books = new ArrayList<>();

        @PodamExclude
        @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
        private List<PrizeEntity> prizes = new ArrayList<>();

        private String name;
        private String description;
        private String image;
}

En esta clase vamos a resaltar las siguientes anotaciones:

En los atributos de la clase tenemos:

Para terminar de enseñar las anotaciones más importantes, abordaremos la clase PrizeEntity. Esta contiene el siguiente código:

package co.edu.uniandes.dse.bookstore.entities;

import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import lombok.Getter;
import lombok.Setter;
import uk.co.jemos.podam.common.PodamExclude;

/**
 * Clase que representa un premio en la persistencia
 *
 * @author ISIS2603
 */

@Getter
@Setter
@Entity
public class PrizeEntity extends BaseEntity {

        @Temporal(TemporalType.DATE)
        private Date premiationDate;

        @PodamExclude
        @ManyToOne
        private AuthorEntity author;

        private String name;
        private String description;

        @PodamExclude
        @OneToOne
        private OrganizationEntity organization;
}


En esta clase vamos a resaltar las siguientes anotaciones:

Elementos importantes a resaltar de las anotaciones y atributos de una Entidad:

En el paquete repositories de nuestro proyecto encontramos toda la capa de persistencia. Cada entidad que hayamos creado debe tener un archivo de persistencia asignado. Este archivo corresponde a una interface que extiende de JpaRepository, el cual nos maneja el CRUD (Create, Retrieve, Update, Delete) de la entidad. La siguiente figura muestra el UML de la capa de persistencia para el recurso Author.

Revisemos el AuthorRepository:

package co.edu.uniandes.dse.bookstore.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import co.edu.uniandes.dse.bookstore.entities.AuthorEntity;

@Repository
public interface AuthorRepository extends JpaRepository<AuthorEntity, Long> {

}

La interfaz está anotada con @Repository, lo que indica que será un repositorio, es decir, que encapsulará el comportamiento de almacenamiento, recuperación y búsqueda que emula una colección de objetos.

La interfaz extiende de JpaRepository. Esto permite que la clase tenga acceso a la API completa de CrudRepository y PagingAndSortingRepository. De este modo, se contará con las operaciones CRUD básicas (crear, obtener, actualizar y borrar) y también operaciones para paginación y ordenamiento.

En los parámetros de la interface genérica JpaRepository se agrega el tipo de la entidad (en este caso AuthorEntity) y el tipo de dato de la clave primaria (en este caso Long).

Cuando queramos hacer métodos específicos de búsqueda en nuestra base de datos, por ejemplo con la estructura SELECT * FROM Tabla WHERE columna=parámetro, debemos crear las definiciones de los métodos en nuestra interfaz. Tomaremos de ejemplo la clase BookRepository que tiene el siguiente código:

package co.edu.uniandes.dse.bookstore.repositories;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import co.edu.uniandes.dse.bookstore.entities.BookEntity;

@Repository
public interface BookRepository extends JpaRepository<BookEntity, Long> {
        List<BookEntity> findByIsbn(String isbn);
}

Como podemos observar, queremos crear una búsqueda de un libro a partir de su isbn adicional al método que trae la interface JpaRepository para buscar un libro por su llave primaria, por lo que debemos declarar el método findByIsbn el cual es implementado automáticamente por la clase que implementa nuestra interface BookRepository y que es creada por el framework.

En el paquete de services encontramos toda la lógica de la aplicación y donde integraremos las reglas de negocio. Para entender el funcionamiento usaremos la clase AuthorService. El siguiente diagrama muestra los paquetes y clases involucradas para el ejemplo del recurso Author.

El siguiente código corresponde a la clase AuthorService. Solamente tiene un método para obtener todos los autores que están en la base de datos. Note que hace un llamado utilizando la interface AuthorRepository al método findAll() que está definido en JpaRepository.

package co.edu.uniandes.dse.bookstore.services;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import co.edu.uniandes.dse.bookstore.entities.AuthorEntity;
import co.edu.uniandes.dse.bookstore.repositories.AuthorRepository;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * Clase que implementa la conexion con la persistencia para la entidad de
 * Author.
 *
 * @author ISIS2603
 */

@Slf4j
@Service
public class AuthorService {

        @Autowired
        AuthorRepository authorRepository;
        
                /**
         * Obtiene la lista de los registros de Author.
         *
         * @return Colección de objetos de AuthorEntity.
         */
        @Transactional
        public List<AuthorEntity> getAuthors() {
                log.info("Inicia proceso de consultar todos los autores");
                return authorRepository.findAll();
        }

}

Veamos las anotaciones en la clase_

Dentro de nuestra capa de lógica, asociaremos métodos de CRUD de nuestra base de datos añadiendo reglas de negocio cuando sean necesarias. Siguiendo con AuthorService tenemos las siguientes funciones:

/**
 * Se encarga de crear un Author en la base de datos.
 *
 * @param author Objeto de AuthorEntity con los datos nuevos
 * @return Objeto de AuthorEntity con los datos nuevos y su ID.
 */
@Transactional
public AuthorEntity createAuthor(AuthorEntity author) {
    log.info("Inicia proceso de creación del autor");
    return authorRepository.save(author);
}

Obtención de todos los Autores:

/**
 * Obtiene la lista de los registros de Author.
 *
 * @return Colección de objetos de AuthorEntity.
 */
@Transactional
public List < AuthorEntity > getAuthors() {
    log.info("Inicia proceso de consultar todos los autores");
    return authorRepository.findAll();
}
/**
 * Obtiene los datos de una instancia de Author a partir de su ID.
 *
 * @param authorId Identificador de la instancia a consultar
 * @return Instancia de AuthorEntity con los datos del Author consultado.
 */
@Transactional
public AuthorEntity getAuthor(Long authorId) throws EntityNotFoundException {
    log.info("Inicia proceso de consultar el autor con id = {0}", authorId);
    Optional < AuthorEntity > authorEntity = authorRepository.findById(authorId);
    if (authorEntity.isEmpty())
        throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);
    log.info("Termina proceso de consultar el autor con id = {0}", authorId);
    return authorEntity.get();
}
/**
 * Actualiza la información de una instancia de Author.
 *
 * @param authorId     Identificador de la instancia a actualizar
 * @param authorEntity Instancia de AuthorEntity con los nuevos datos.
 * @return Instancia de AuthorEntity con los datos actualizados.
 */
@Transactional
public AuthorEntity updateAuthor(Long authorId, AuthorEntity author) throws EntityNotFoundException {
    log.info("Inicia proceso de actualizar el autor con id = {0}", authorId);
    Optional < AuthorEntity > authorEntity = authorRepository.findById(authorId);
    if (authorEntity.isEmpty())
        throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);
    log.info("Termina proceso de actualizar el autor con id = {0}", authorId);
    author.setId(authorId);
    return authorRepository.save(author);
}
/**
 * Elimina una instancia de Author de la base de datos.
 *
 * @param authorId Identificador de la instancia a eliminar.
 * @throws BusinessLogicException si el autor tiene libros asociados.
 */
@Transactional
public void deleteAuthor(Long authorId) throws IllegalOperationException, EntityNotFoundException {
    log.info("Inicia proceso de borrar el autor con id = {0}", authorId);
    Optional < AuthorEntity > authorEntity = authorRepository.findById(authorId);
    if (authorEntity.isEmpty())
        throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);

    List < BookEntity > books = authorEntity.get().getBooks();
    if (!books.isEmpty())
        throw new IllegalOperationException("Unable to delete the author because he/she has associated books");

    List < PrizeEntity > prizes = authorEntity.get().getPrizes();
    if (!prizes.isEmpty())
        throw new IllegalOperationException("Unable to delete the author because he/she has associated prizes");

    authorRepository.deleteById(authorId);
    log.info("Termina proceso de borrar el autor con id = {0}", authorId);
}

En este último código manejamos la regla de negocio donde no se puede eliminar un autor si este tiene libros o premios asociados. Primero deben ser borrados los libros y premios del autor para borrar el autor en la base de datos. Fíjese que cuando una regla de negocio ocurre, arrojamos la excepción de IllegalOperationException

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. Nos centraremos en explicar la asociación de Autor y Book en la clase AuthorBookService.

La clase está definida de la siguiente forma:

@Slf4j
@Data
@Service
public class AuthorBookService {

        @Autowired
        private BookRepository bookRepository;

        @Autowired
        private AuthorRepository authorRepository;

      ...
}

Lo importante a resaltar, es que 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:

Agregar un libro a un autor dados sus id:

@Transactional
public BookEntity addBook(Long authorId, Long bookId) throws EntityNotFoundException {
    log.info("Inicia proceso de asociarle un libro al autor con id = {0}", authorId);
    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().add(authorEntity.get());
    log.info("Termina proceso de asociarle un libro al autor con id = {0}", authorId);
    return bookEntity.get();
}

Aquí manejamos los errores cuando no se encuentra el autor o el libro especificado por parámetro.

Obtener libros de un Autor dado su id:

@Transactional
public List < BookEntity > getBooks(Long authorId) throws EntityNotFoundException {
    log.info("Inicia proceso de consultar todos los libros del autor con id = {0}", authorId);
    Optional < AuthorEntity > authorEntity = authorRepository.findById(authorId);
    if (authorEntity.isEmpty())
        throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);

    List < BookEntity > books = bookRepository.findAll();
    List < BookEntity > bookList = new ArrayList < > ();

    for (BookEntity b: books) {
        if (b.getAuthors().contains(authorEntity.get())) {
            bookList.add(b);
        }
    }
    log.info("Termina proceso de consultar todos los libros del autor con id = {0}", authorId);
    return bookList;
}

Aquí revisamos en primer lugar que el autor dado por parámetro exista. Una vez confirmada su existencia, se consultan todos los libros y se miran cuales de ellos están asociados al autor especificado. Los que tienen el autor asociado son agregados a una lista de libros que será retornada al final.

Obtención de un libro de un Autor dados sus ids:

@Transactional
public BookEntity getBook(Long authorId, Long bookId) throws EntityNotFoundException, IllegalOperationException {
    log.info("Inicia proceso de consultar el libro con id = {0} del autor con id = " + authorId, 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 el libro con id = {0} del autor con id = " + authorId, bookId);
    if (bookEntity.get().getAuthors().contains(authorEntity.get()))
        return bookEntity.get();

    throw new IllegalOperationException("The book is not associated to the author");
}

Actualización de libros de un Autor:

@Transactional
public List < BookEntity > addBooks(Long authorId, List < BookEntity > books) throws EntityNotFoundException {
    log.info("Inicia proceso de reemplazar los libros asociados al author con id = {0}", authorId);
    Optional < AuthorEntity > authorEntity = authorRepository.findById(authorId);
    if (authorEntity.isEmpty())
        throw new EntityNotFoundException(ErrorMessage.AUTHOR_NOT_FOUND);

    for (BookEntity book: books) {
        Optional < BookEntity > bookEntity = bookRepository.findById(book.getId());
        if (bookEntity.isEmpty())
            throw new EntityNotFoundException(ErrorMessage.BOOK_NOT_FOUND);

        if (!bookEntity.get().getAuthors().contains(authorEntity.get()))
            bookEntity.get().getAuthors().add(authorEntity.get());
    }
    log.info("Finaliza proceso de reemplazar los libros asociados al author con id = {0}", authorId);
    return books;
}

Desasociar libro de un autor dados sus ids:

@Transactional
public void removeBook(Long authorId, Long bookId) throws EntityNotFoundException {
    log.info("Inicia proceso de borrar un libro del author con id = {0}", authorId);
    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().remove(authorEntity.get());
    log.info("Finaliza proceso de borrar un libro del author con id = {0}", authorId);
}

Es importante notar que para este ejemplo las asociaciones en la lógica también son recíprocas, es decir existe un AuthorBookService y un BookAuthorService, ya que cada una maneja la lógica y reglas de negocio según sus necesidades. Estuvimos observando la lógica de la relación ManyToMany entre Author y Book, las demás relaciones en la lógica tienen una forma parecida. Para ver una implementación de la asociación en la capa de lógica de tipo OneToMany, puede ver las clases de BookEditorialService y EditorialBookService, ya que una editorial tiene muchos libros pero un libro solo tiene asignada una editorial.

Vaya a la carpeta, src/test/java > paquete services. En esta encontraremos todos los archivos JUnit de prueba de lógica de nuestro proyecto. Si queremos crear un nuevo archivo de pruebas, lo hacemos con click derecho en el paquete > New > Other > JUnit > JUnit Test Case.

Seleccione la opción New JUnit Jupiter Test. En el nombre de la clase escriba {{Nombre_Entidad}}ServiceTest. Note que el sufijo ServiceTest será la convención de nombramiento para las clases de prueba de lógica.

Asegúrese de que la opción @BeforeEach setUp() esté marcada. Luego en la opción Class under test busque la clase {{Nombre_Entidad}}Service y haga clic en Next. En el siguiente paso marque los métodos que usará en la prueba. Generalmente se debe probar todo el CRUD dentro de la prueba por lo que marcaremos todos los del Servicio.

Las clases se debe anotar con:

@ExtendWith(SpringExtension.class). Indica que la clase de pruebas extiende de Spring

@DataJpaTest. Indica que en la prueba se involucra 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 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 debe agregar una referencia a Podam:

private PodamFactory factory = new PodamFactoryImpl();

Podam es una librería que permite crear nuevos objetos con datos ficticios.

Generalmente se debe definir una lista de Entidades al servicio a probar:

private List<{{Nombre_Entidad}}Entity> {{nombre_entidad}}List = new ArrayList<>();

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 la tabla {{Nombre_Entidad}}Entity y el segundo se encarga de insertar tres registros con ayuda del EntityManager y se guardan en la lista {{nombre_entidad}}List. Para seguir entendiendo los métodos de la clase de pruebas, usaremos AuthorServiceTest definida por el momento de la siguiente forma:

package co.edu.uniandes.dse.bookstore.service;

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

import java.util.ArrayList;
import java.util.List;

import javax.transaction.Transactional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import co.edu.uniandes.dse.bookstore.entities.AuthorEntity;
import co.edu.uniandes.dse.bookstore.entities.BookEntity;
import co.edu.uniandes.dse.bookstore.entities.PrizeEntity;
import co.edu.uniandes.dse.bookstore.exceptions.EntityNotFoundException;
import co.edu.uniandes.dse.bookstore.exceptions.IllegalOperationException;
import co.edu.uniandes.dse.bookstore.services.AuthorService;
import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl;

@ExtendWith(SpringExtension.class)
@DataJpaTest
@Transactional
@Import(AuthorService.class)
class AuthorServiceTest {

    @Autowired
    private AuthorService authorService;

    @Autowired
    private TestEntityManager entityManager;

    private PodamFactory factory = new PodamFactoryImpl();

    private List < AuthorEntity > authorList = new ArrayList < > ();

    @BeforeEach
    void setUp() {
        clearData();
        insertData();
    }

    private void clearData() {
        entityManager.getEntityManager().createQuery("delete from PrizeEntity").executeUpdate();
        entityManager.getEntityManager().createQuery("delete from BookEntity").executeUpdate();
        entityManager.getEntityManager().createQuery("delete from AuthorEntity").executeUpdate();
    }

    private void insertData() {
        for (int i = 0; i < 3; i++) {
            AuthorEntity authorEntity = factory.manufacturePojo(AuthorEntity.class);
            entityManager.persist(authorEntity);
            authorList.add(authorEntity);
        }

        AuthorEntity authorEntity = authorList.get(2);
        BookEntity bookEntity = factory.manufacturePojo(BookEntity.class);
        bookEntity.getAuthors().add(authorEntity);
        entityManager.persist(bookEntity);

        authorEntity.getBooks().add(bookEntity);

        PrizeEntity prize = factory.manufacturePojo(PrizeEntity.class);
        prize.setAuthor(authorList.get(1));
        entityManager.persist(prize);
        authorList.get(1).getPrizes().add(prize);
    }
   ...
}

Como podemos observar, tenemos la inyección del servicio AuthorService y la del EntityManager, al igual que tenemos la referencia la Podam y una lista de AuthorEntity donde guardamos la información. El método setUp() se ejecuta antes de ejecutar la prueba y este a su vez llama los metodos clearData() e insertData(). En el metoo clearData() limpiamos todas las tablas que se relacionen con la entidad AuthorEntity, en este caso borramos PrizeEntity, BookEntity y AuthorEntity. Lueg de haber limpiado la base de datos, insertData() crea 3 AuthorEntity arbitrarios usando la libreria Podam. Recuerde que los atributos marcados con PodamExclude enla entidad no son autegenerados y siempre son inicializados en null. Seguiremos con la prueba de creación de un autor.

Test creación de un Autor:

@Test
void testCreateAuthor() {
    AuthorEntity newEntity = factory.manufacturePojo(AuthorEntity.class);
    AuthorEntity result = authorService.createAuthor(newEntity);
    assertNotNull(result);

    AuthorEntity entity = entityManager.find(AuthorEntity.class, result.getId());

    assertEquals(newEntity.getId(), entity.getId());
    assertEquals(newEntity.getName(), entity.getName());
    assertEquals(newEntity.getBirthDate(), entity.getBirthDate());
    assertEquals(newEntity.getDescription(), entity.getDescription());
}

Como podemos observar, creamos un AuthorEntity con datos arbitrarios y luego buscamos que se haya persistido en la base de datos. Estos métodos de prueba no deben llevar private o public, todos deben estar sentenciados con void(), además de tener todos la anotación @Test de JUnit.

Test obtener Autores:

@Test
void testGetAuthors() {
    List < AuthorEntity > authorsList = authorService.getAuthors();
    assertEquals(authorList.size(), authorsList.size());

    for (AuthorEntity authorEntity: authorsList) {
        boolean found = false;
        for (AuthorEntity storedEntity: authorList) {
            if (authorEntity.getId().equals(storedEntity.getId())) {
                found = true;
            }
        }
        assertTrue(found);
    }
}

Como podemos observar, el código, obtiene todos los autores de la base de datos y los compara con los que tiene en memoria en authorList. Deben estar los mismos registros por lo que este resultado al final debe ser true.

Test obtener Autor dado su Id:

@Test
void testGetAuthor() throws EntityNotFoundException {
    AuthorEntity authorEntity = authorList.get(0);

    AuthorEntity resultEntity = authorService.getAuthor(authorEntity.getId());
    assertNotNull(resultEntity);

    assertEquals(authorEntity.getId(), resultEntity.getId());
    assertEquals(authorEntity.getName(), resultEntity.getName());
    assertEquals(authorEntity.getBirthDate(), resultEntity.getBirthDate());
    assertEquals(authorEntity.getDescription(), resultEntity.getDescription());
}

Como podemos observar, utilizamos el primer autor de la lista para consultar en la base de datos dado su Id. Estos deben coincidir en todos los atributos, por lo que hacemos los respectivos chequeos.

Test obtener un Autor que no existe:

@Test
void testGetInvalidAuthor() {
    assertThrows(EntityNotFoundException.class, () - > {
        authorService.getAuthor(0 L);
    });
}

Como podemos observar, tenemos que buscar un registro en la base de datos que no existe, por lo que lo hacemos con el Id 0. De esto se debe esperar que tire la excepción de entidad no encontrada, por lo que el método debe estar escrito de esta forma. Si observamos con detenimiento, podrá notar que se hace uso de la notación lambda "->", el cual crea una función anónima en Java, es decir, el método assertThrows() recibe 2 parametros, el primero es la excepción que se espera que lance, y el segundo es una función que dispare esta excepción. En este caso la función no recibe parámetros y solo hace la instrucción de authorService.getAuthor(0 L);.

Test actualizar Autor:

@Test
void testUpdateAuthor() throws EntityNotFoundException {
    AuthorEntity authorEntity = authorList.get(0);
    AuthorEntity pojoEntity = factory.manufacturePojo(AuthorEntity.class);

    pojoEntity.setId(authorEntity.getId());

    authorService.updateAuthor(authorEntity.getId(), pojoEntity);

    AuthorEntity response = entityManager.find(AuthorEntity.class, authorEntity.getId());

    assertEquals(pojoEntity.getId(), response.getId());
    assertEquals(pojoEntity.getName(), response.getName());
    assertEquals(pojoEntity.getBirthDate(), response.getBirthDate());
    assertEquals(pojoEntity.getDescription(), response.getDescription());
}

En este caso creamos nuevos atributos arbitrarios con la librería Podam y los actualizamos a un registro en memoria. Finalmente, persistimos la información y verificamos que todo se haya actualizado correctamente.

Test actualizar un Autor invalido (No existente):

@Test
void testUpdateInvalidAuthor() {
    assertThrows(EntityNotFoundException.class, () - > {
        AuthorEntity pojoEntity = factory.manufacturePojo(AuthorEntity.class);
        authorService.updateAuthor(0 L, pojoEntity);
    });
}

Aquí también se hace uso de la notación lambda y tiene la misma lógica que testGetInvalidAuthor().

Por último las pruebas de borrado:

Test Borrar un Autor:

@Test
void testDeleteAuthor() throws EntityNotFoundException, IllegalOperationException {
    AuthorEntity authorEntity = authorList.get(0);
    authorService.deleteAuthor(authorEntity.getId());
    AuthorEntity deleted = entityManager.find(AuthorEntity.class, authorEntity.getId());
    assertNull(deleted);
}

En esta prueba borramos el registro y verificamos que al intentar buscarlo este sea null al no existir en la base de datos.

Test Borrar un Autor Inválido:

@Test
void testDeleteInvalidAuthor() {
    assertThrows(EntityNotFoundException.class, () - > {
        authorService.deleteAuthor(0 L);
    });
}

Test borrar un Autor con libros asociados:

@Test
void testDeleteAuthorWithBooks() {
    assertThrows(IllegalOperationException.class, () - > {
        authorService.deleteAuthor(authorList.get(2).getId());
    });
}

Este último método está probando una regla de negocio. Se espera que el estudiante cree métodos similares con el objetivo de infringir reglas de negocio y conocer si se están lanzando las excepciones correspondientes.

Test borrar un Autor con premios asociados:

@Test
void testDeleteAuthorWithPrize() {
    assertThrows(IllegalOperationException.class, () - > {
        authorService.deleteAuthor(authorList.get(1).getId());
    });
}

Este último método está probando una regla de negocio. Se espera que el estudiante cree métodos similares con el objetivo de infringir reglas de negocio y conocer si se están lanzando las excepciones correspondientes.