Llegará el día en que la lógica de negocio no se encuentre en la base de datos, en que hibernate nos abstraiga de preocuparnos por la queries, pero hoy no es ese día, en este día nuestras queries son complicadas, son importantes, tienen varios niveles de profundidad y lo que es peor, cada dos por tres alguien está metiendo un campo nuevo y rompiéndolas.
¿Y cuál es la mejor solución a estos problemas?
Refactoriza todo tu sistema y traslada la lógica de negocio a tus casos de uso.
¿Y cuál es la segunda mejor solución a estos problemas?
Métele tests. Es más, la forma más segura de refactorizar todo tu sistema va a pasar por meterle tests.
Hemos montado un ejemplo muy sencillito (https://github.com/carlos-devanddel/DBUnitTest) para mostrar cómo llevar a cabo la configuración del entorno para poder llevar a cabo los tests sobre las queries.
Como decíamos, es muy sencillo, se compone un DAO muy simple, que recupera un objeto de base de datos por ID.
@Repository
public class TheDAO {
@Autowired
private JdbcTemplate jdbcTemlate;
public TheModel getDataById(Integer id) throws NotFoundException {
try {
return jdbcTemlate.queryForObject("SELECT * FROM MODELS WHERE id = " + id,(rs, rowNum) ->
new TheModel(rs.getInt("ID"), rs.getString("DESCRIPTION"))
);
}catch (EmptyResultDataAccessException e){throw new NotFoundException();}
}
}
El modelo como podemos ver es un POJO que se compone de un ID y una descripción.
Dependencias que necesitamos
Como dijimos al principio, vamos a utilizar la BBDD de H2, por lo que vamos a necesitar dicha dependencia
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
Para incluir las anotaciones de JPA que nos van a ayudar a crear el esquema inicial de la base de datos también necesitaremos la siguiente dependencia.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Montar el esquema inicial
Una vez hemos incluido las dependencias necesarias, necesitamos decirle a H2 cuál va a ser el esquema de la BBDD. Para ello vamos a aprovechar las anotaciones de JPA y el hecho de que nuestro modelo es una representación directa de la tabla de base de datos.
@Entity
@Table(name = "MODELS")
public class TheModel {
@Id
@Column(name = "ID")
private Integer id;
@Column(name = "DESCRIPTION")
private String description;
De esta forma, anotamos la clase como @Entity, le indicamos el nombre de la tabla en @Table y los nombres de cada columna con @Column. Y con esto ya H2 va a montar una base de datos con una tabla MODELS con dos campos, uno ID y uno DESCRIPTION. Lo siguiente, testear.
Testeando
Estos tests van a estar en la frontera entre unitarios y de integración. Según mi punto de vista son unitarios, porque lo que busco es probar que la query se comporta como quiero, pero se podrían considerar de integración porque se pasa la frontera del código a la bbdd.
Sea como sea, para poder ejecutarlos, lo primero vamos a necesitar que Spring monte un contexto para los tests, por lo que necesitaremos, en la misma carpeta en la que están los tests, una clase de configuración con la anotación de @SpringBootApplication y la anotación @EntityScan en la que le indicamos la ruta al paquete en el que están nuestros modelos con las anotaciones para crear el esquema.
@SpringBootApplication
@EntityScan("dyd.pocs.dbunittest.models")
public class DBTestConfig {}
Una vez listo esto podemos ponernos a nuestros tests, añadiendo un @SpringBootTest e inyectando el DAO desde el que vamos a llamar a nuestras queries.
@SpringBootTest
class TheDAOTest {
@Autowired
private TheDAO sut;
@Test
@Sql(statements = "INSERT INTO MODELS (ID, DESCRIPTION) VALUES(1, 'Descripcion')")
void getDataById_should_returnModel_when_Exists() throws Exception {
Integer inputId = 1;
TheModel result = sut.getDataById(inputId);
Assertions.assertNotNull( result);
Assertions.assertEquals(1, result.getId());
Assertions.assertEquals("Descripcion", result.getDescription());
}
@Test
void getDataById_should_throw_NotFoundException_when_modelDoesntExists(){
Assertions.assertThrows(NotFoundException.class, () -> sut.getDataById(2));
}
}
Si descomponemos los test en sus tres partes básicas:
Given: preparación de los datos
Aquí está lo único especial que habría que hacer en estos tests, como podemos ver al inicio del primer test tenemos un @Sql, esto es para insertar los datos que esperamos sacar con nuestra consulta.
When: ejecución del sut
Hacemos la llamada al dao con los datos de entrada y recogemos la salida en el objeto result.
Then: comprobación de los resultados
Hacemos las aserciones que queramos sobre el resultado de la ejecución
Pero… es que mis modelos no coinciden con la BD.
Ya sea porque tus modelos no coinciden con la bbdd y no puedes colocar las anotaciones para que te cree el esquema, o que el cliente en el que estás tenga un miedo irracional a la dependencia de JPA, tienes la alternativa de inicializar tu bbdd H2 con un fichero ddl con los CREATE TABLE que necesites para representar tu esquema.
En este caso la configuración sería más sencilla, no necesitaríamos el fichero de configuración con la anotación @EntityScan, ni la dependencia de JPA, ni las anotaciones en los modelos. En su lugar necesitamos el esquema de inicialización de la bbdd en el fichero test/resources/schema.sql
CREATE TABLE MODELS (
ID INTEGER NOT NULL PRIMARY KEY,
DESCRIPTION VARCHAR(200)
);
En el repositorio está disponible un ejemplo con esta configuración en la rama initial_schema_ddl.
... yo en realidad lo que tengo es un DB2 y uso cosas del dialecto como LIST_AGG que no funcionan en H2
En este caso tienes como alternativa TestContainers, que con Docker te podría levantar una instancia de tu BD concreta en lugar del H2. El problema de esto es que es un proceso mucho más lento y con una configuración distinta, por lo que lo postergamos para un próximo artículo.