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.
Para realizar este tutorial Ud. debe:
El proyecto del curso está dividido en varias capas. En este tutorial abordaremos las capas correspondientes a la lógica y la persistencia junto con las pruebas de la lógica.
El proyecto, en su vista de desarrollo está organizado en varios paquetes. El paquete controllers incluye las clases que representan los endpoints para el API REST. El paquete services contiene las clases encargadas de la lógica. Finalmente, el paquete repositories contiene las clases encargadas de la persistencia.
Usaremos el siguiente diagrama, para un solo recurso Student
, para identificar las clases involucradas en la implementación de la persistencia y la lógica.
BaseEntity
es una clase abstracta de donde heredan todas las entidades, define el atributo id
anotado con @Id
que representa el identificador único. StudentEntity
está anotada con @Entity y
define los atributos que se requieren para la entidad estudiante, en el ejemplo solo dejamos el atributo name
. StudentRepository
se podrá crear, obtener, actualizar y borrar estudiantes en la base de datos. La interface StudentRepository
hereda de la interface JpaRepository
que contiene las signaturas de los métodos básicos para acceder a la base de datos. StudentRepository
es generada automáticamente por el framework Spring Boot
. Esa clase es responsable de realizar la conexión y de comunicarse con la base de datos. Todas las operaciones CRUD necesarias están implementadas, así que frecuentemente encontraremos que la interface de persistencia estará vacía si no tenemos métodos particulares de acceso a la base de datos, por ejemplo con consultas más complejas. StudentService
, se comunica con la persistencia para saber que tipo de restricciones o reglas de negocio son necesarios para la creación, actualización, borrado u obtención de un registro de una tabla en la base de datos.StudentServiceTest
es la clase que contiene los métodos de prueba de la lógica, es decir, de la clase StudentService.
Para este ejemplo se tiene el concepto Student, por tanto la clase que define la entidad tendrá la convención de nombramiento concepto + Entity, en este caso, StudentEntity
.
Vaya al paquete entities
y con clic derecho seleccione New > Class. Ingrese el nombre de la clase y haga clic en Finish.
La clase debe incluir varias anotaciones:
@Getter y @Setter
. Anotaciones proporcionadas por la librería lombok
que generará en la clase los getters y setters por cada atributo. Estas anotaciones reducen el tamaño de la clase.
@Entity
. Indica que la clase es una entidad que será mapeada a una tabla en la base de datos.
La clase extiende de BaseEntity
para heredar la definición de la llave única.
package co.edu.uniandes.dse.clubcocina.entities;
import javax.persistence.Entity;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class StudentEntity extends BaseEntity {
private String name;
}
En el paquete repositories incluya una nueva interfaz con el nombre StudentRepository
. Note que el sufijo Repository será la convención de nombramiento para las interfaces de la persistencia.
La interfaz debe ser 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 se 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 clase genérica JpaRepository
se agrega el tipo de la entidad (en este caso StudentEntity
) y el tipo de dato de la clave primaria (en este caso Long
).
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.StudentEntity;
/**
* Interface that persists an author
*
* @author ISIS2603
*
*/
@Repository
public interface StudentRepository extends JpaRepository<StudentEntity, Long> {
}
Note que nosotros definimos una interface, la clase que la implementa es generada por el framework.
En el paquete services incluya una nueva clase con el nombre StudentService
. Note que el sufijo Service será la convención de nombramiento para las clases de la lógica.
Esta clase estará anotada con @Service que indica que la clase es un bean encargado de la lógica empresarial.
Incluya un atributo de tipo StudentRepository
anotado por @Autowired
. Esto significa que se usará una inyección de dependencias y que el ciclo de vida de ese repositorio estará a cargo del contenedor de aplicaciones.
En esta clase se espera que se incluyan los métodos del CRUD que son los siguientes:
createStudent
: crea un nuevo estudiante en la base de datos a partir de un objeto de la clase StudentEntity
que recibe de argumento.
getStudents
: obtiene la lista de todos los estudiantes de la base de datos y los retorna como una lista de objetos de la clase StudentEntity
getStudent
: obtiene de la base de datos, el estudiante que tiene como id
el que recibe de argumento. Retorna un objeto de la clase StudentEntity
updateStudent
: actualiza un estudiante, el estudiante que tiene como id
el que recibe de argumento, en la base de datos a partir de un objeto de la clase StudentEntity
que recibe de argumento.
deleteStudent
: elimina el estudiante de la base de datos que tiene como id
el que recibe de argumento.
package co.edu.uniandes.dse.bookstore.services;
import java.util.List;
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.StudentEntity;
import co.edu.uniandes.dse.bookstore.repositories.StudentRepository;
/**
* Clase que implementa los servicios de la lógica de la entidad Student.Para esto, se conecta a la clase que maneja la persistencia StudentRepository.
*
* @author ISIS2603
*/
@Service
public class StudentService {
@Autowired
StudentRepository studentRepository;
/**
* Obtiene la lista de los registros de Student.
*
* @return Colección de objetos de StudentEntity.
*/
@Transactional
public List<StudentEntity> getStudents() {
return studentRepository.findAll();
}
}
En el código anterior se incluye únicamente el método getStudents
. Este método devuelve en una lista de StudentEntity
todos los estudiantes que están en la base de datos. Para esto se usa el método findAll
que está definido en la interface StudentRepository
dado que esta hereda de JpaRepository
. Asegúrese de anotar todos los métodos de la lógica con @Transactional.
Vaya a la carpeta, src/test/java > paquete services > click derecho > New > Other > JUnit > JUnit Test Case.
Seleccione la opción New JUnit Jupiter Test
. En el nombre de la clase escriba StudentServiceTest
. Note que el sufijo ServiceTest será la convención de nombramiento para las clases de prueba de la lógica.
Asegúrese de que la opción @BeforeEach setUp
esté marcada. Luego en la opción Class under test
busque la clase StudentService
y haga clic en Next. En el siguiente paso marque el método getStudents
y haga clic en Finish
.
Anote la clase 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(StudentService.class)
. Indica la clase del servicio que se probará en el test.
Inyecte dos dependencias:
@Autowired
private StudentService studentService;
@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 base de datos directamente).
Agregue una referencia a Podam:
private PodamFactory factory = new PodamFactoryImpl();
Podam es una librería que permite crear nuevos objetos con datos ficticios.
Luego, defina una lista para almacenar un conjunto de estudiantes que crearemos con los datos de prueba.
private List<StudentEntity> studentList = 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()
.
clearData()
borra los datos de la tabla StudentEntity.
Para esto se utiliza el entityManager
y se envía a la base de datos un query para borrar los datos de la tabla: "delete from StudentEntity"
/**
* Limpia las tablas que están implicadas en la prueba.
*/
private void clearData() {
entityManager.getEntityManager().createQuery("delete from StudentEntity").executeUpdate();
}
insertData()
se encarga de insertar tres registros con ayuda del EntityManager
y se guardan en la lista studentList
. Utilizando Podam
se crea un objeto StudentEntity
con datos aleatorios. Luego se persiste en la base de datos usando en entityManager
. Esto crea el registro en la tabla y le asigna un único id
. Ese objeto, con el id creado se agrega a la lista studentList.
private void insertData() {
for (int i = 0; i < 3; i++) {
StudentEntity studentEntity = factory.manufacturePojo(StudentEntity.class);
entityManager.persist(studentEntity);
studentList.add(studentEntity);
}
}
Finalmente se implementa la prueba en el método testGetStudents
, que está anotado con @Test. En el cuerpo del método se llama al método getStudents
del servicio y se guarda el resultado en una lista. En este ejemplo, la única prueba que se hace es comparar el tamaño de esa lista con la que se definió inicialmente; los tamaños deben coincidir para que la prueba sea exitosa.
package co.edu.uniandes.dse.clubcocina.services;
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.clubcocina.entities.StudentEntity;
import co.edu.uniandes.dse.clubcocina.services.StudentService;
import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl;
/**
* Pruebas de lógica de Student
*
* @author ISIS2603
*/
@ExtendWith(SpringExtension.class)
@DataJpaTest
@Transactional
@Import(StudentService.class)
class StudentServiceTest {
@Autowired
private StudentService studentService;
@Autowired
private TestEntityManager entityManager;
private PodamFactory factory = new PodamFactoryImpl();
private List<StudentEntity> studentList = 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 StudentEntity").executeUpdate();
}
/**
* Inserta los datos iniciales para el correcto funcionamiento de las pruebas.
*/
private void insertData() {
for (int i = 0; i < 3; i++) {
StudentEntity studentEntity = factory.manufacturePojo(StudentEntity.class);
entityManager.persist(studentEntity);
studentList.add(studentEntity);
}
}
/**
* Prueba para consultar la lista de Student.
*/
@Test
void testGetStudents() {
List<StudentEntity> list = studentService.getStudents();
assertEquals(list.size(), studentList.size());
}
}
Para ejecutar la prueba, desde el explorador de proyectos haga clic en el archivo de la prueba y seleccione Run As > JUnit test. Esto desplegará un tab denominado JUnit con el resultado de la ejecución. Si todo está correcto no deben aparecer ni Errors ni Failures.