
Introducción
Muchas veces, cuando queremos testear una aplicación, nos encontramos con los mismos problemas y preguntas: ¿Cómo puedo testear esto? ¿Es una buena aproximación? ¿Debería usar mocks? ¿Tiene sentido ejecutar tests directamente contra la base de datos?
Como casi siempre, la respuesta es: Depende. No todos los retos se pueden abordar de la misma forma ni con el mismo nivel de éxito. Aun así, vamos a repasar algunos conceptos que pueden ayudarte a tomar mejores decisiones al testear tus proyectos.
Vamos a asumir que conoces la diferencia entre unit tests y e2e tests. Pero cuando tienes que comprobar tus consultas a base de datos, ¿cómo lo has hecho hasta ahora?
Según nuestra experiencia, muchos desarrolladores consideran que los e2e tests son una buena opción para comprobar consultas a base de datos porque no necesitan tener en cuenta los factores del dominio de la aplicación, y se limitan a basar los tests en fotografías de datos de tablas que devuelven distintos estados de la aplicación.
Otra aproximación es crear unit tests para el consumidor del repositorio usando un mock del mismo y confiar en que funciona, porque no forma parte de nuestro dominio. En realidad forma parte de la infraestructura que hemos elegido.
Ambas opciones son válidas. Sin embargo, cada una implica cosas diferentes:
- Siguiendo la aproximación de e2e tests, veremos que este tipo de tests son demasiado lentos, frágiles y no contemplan todos los casos posibles. Además, si añades más casos de prueba, dedicarás más tiempo a mantenerlos y ejecutarlos.
- A nivel de unit tests comprobarás que tu dominio funciona, pero no cubrirás posibles fallos introducidos por la consulta a base de datos. Esto es un riesgo importante en proyectos donde la base de datos es uno de los factores clave del negocio. Si esta aproximación es buena o no, eso ya es otro debate.
Entonces… ¿qué podemos hacer? Hoy vamos a introducir el concepto de Slicing.
¿Qué es un Slice test?
Un slice test es un tipo de integration test. Como indica el nombre, sigue una estrategia de corte por secciones.
¿Cómo? Tomamos la parte más aislada de la aplicación que necesitamos probar y le añadimos un entorno específico lo más cercano posible a los factores reales donde ese código se va a ejecutar.
Al leer esto probablemente hayas pensado algo como: “Vale, entonces propones tener una base de datos específica para testing”. Sí, exactamente, aunque se puede hacer de distintas maneras. Puedes:
- Tener una base de datos aislada para testing, con recursos distintos y configuraciones simples.
- Tener una base de datos en memoria distinta de la real. Sin embargo, eso probablemente implique usar tecnologías diferentes o introducir cambios en el sistema de consultas, así que no suele ser una solución adecuada.
- Usar la base de datos real añadiendo alguna columna adicional para identificar si los datos fueron insertados con propósito de testing y poder eliminarlos después.
Sí, puedes usar cualquiera de esas opciones si te funciona. Aun así, tenemos una propuesta diferente:
¿Qué pasaría si en tu dockerfile de base de datos pudieras crear un esquema con propósito de testing para no incrementar el espacio en disco y que además:
- Use tu mismo sistema de migraciones.
- Tenga los mismos recursos que tu aplicación.
- Se utilice en el entorno correspondiente.
- Todos los datos de testing no afecten a los reales.
- Y todos los datos almacenados desaparezcan cuando terminen tus tests.
Eso es posible y vamos a ver cómo hacerlo.
Requisitos
Para el siguiente ejemplo usaremos una base de datos PostgreSQL con un backend en Kotlin usando SpringBoot y Liquibase como herramienta de migraciones. Aun así, los conceptos que vamos a explicar son muy parecidos en otras herramientas y lenguajes como MongoDB, Typescript o Express.
Creando la base de datos
En primer lugar, asumiendo que utilizas un fichero docker-compose.yml para preparar tu entorno local, necesitarás una estrategia para crear múltiples esquemas o bases durante la creación de la base de datos.
Con Postgresql usaremos la aproximación sugerida en este repositorio de Github. La idea principal es proporcionar un fichero .sh que se ejecutará la primera vez para crear automáticamente varias bases de datos.
Para ello tendrás que añadir una nueva variable de entorno llamada POSTGRES_MULTIPLE_DATABASES. Puedes concatenar varios valores separados por comas con los nombres de esas bases de datos. En nuestro caso definiremos dos: la real y la misma añadiendo el sufijo _test:
version: '3'
services:
database:
image: "postgres"
ports:
- "5432:5432"
env_file:
- docker/database/database.env
volumes:
- database-data:/var/lib/postgresql/data/
- ./docker/database/init-scripts:/docker-entrypoint-initdb.d
Dado el fichero database.env:
POSTGRES_USER=afergon
POSTGRES_PASSWORD=afergon
POSTGRES_MULTIPLE_DATABASES=comic_libraries,comic_libraries_test
Descarga el script proporcionado en el repositorio de Github.
💡 Si no usas PostgreSQL, investiga cómo conseguir algo equivalente con tu tecnología de base de datos actual.
Configurando las migraciones
Este es un paso opcional, pero si utilizas migraciones agradecerás ejecutarlas también sobre tu nueva base de datos de testing. Hemos elegido una aplicación SpringBoot en la que usamos Liquibase para las migraciones.
Puedes ver cómo hacerlo siguiendo este post.
Lo único que tendrás que tener en cuenta es crear también un application.property o application.yml para tus tests.
Conectando con la base de datos de test
Para conectarte a la base de datos de test tendrás que sobrescribir el fichero de configuración que usa tu aplicación. En Spring esto es muy sencillo. Solo necesitas crear un nuevo fichero application.yml en tu directorio de recursos de test con el siguiente contenido:
spring:
test:
database:
replace: none
datasource:
driver-class-name: org.postgresql.Driver
password: afergon
url: jdbc:postgresql://localhost:5432/comic_libraries_test
username: afergon
liquibase:
change-log: classpath:database/liquibase-changelog.xml
A partir de ese momento todos tus tests apuntarán a la base de datos de testing.
Eliminando datos
La estrategia para eliminar datos consiste en mantener cada test dentro de una transacción y hacer un roll-back al terminar cada uno. Esto mantendrá la consistencia de los datos y la base de datos siempre limpia, porque tu test nunca llegará a escribir de forma persistente en las tablas.
Con JPA esto es muy sencillo: solo necesitas añadir la anotación @DataJpaTest en la clase de test y hará el trabajo por ti. Si no, siempre puedes implementar tus propios métodos beforeEach y afterEach.
Creando el test
El paso final es crear un test. Vamos a mantenerlo simple. Comprobemos el método find all que viene por defecto con Hibernate y verifiquemos que todo funciona:
package dev.afergon.kotlinhibernateperformance.application.repositories
import dev.afergon.kotlinhibernateperformance.application.entities.Library
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
@DataJpaTest
internal class LibraryRepositorySliceTest {
@Autowired
lateinit var libraryRepository: LibraryRepository
@Test
fun `should returns list of libraries`() {
libraryRepository.save(Library(name = "irrelevant name"))
val actual = libraryRepository.findAll()
assertEquals(actual.size, 1)
}
}
Y eso es todo. A partir de ahora puedes implementar consultas complejas y revisar todos los casos límite de tus queries sin miedo a romper la base de datos.
Conclusión
Seguir la estrategia de divide y vencerás siempre nos permite reducir la complejidad y el mantenimiento de las aplicaciones. También puede aplicarse a los tests y te ayudará a mantener su alcance lo más simple posible.
Aislar las consultas de nuestros tests de la capa de dominio permitirá al equipo mantener el foco en las capas de negocio sin necesidad de ser un gurú de la tecnología de base de datos.
Puedes echar un vistazo al código, y a más ejemplos, en el repositorio de Github.
Espero que este post te ayude a resolver algunas dudas y a seguir mejorando el alcance de los tests en tu aplicación.
Muchas gracias por leerlo y no olvides compartirlo si te ha resultado útil.
Gracias a Mireia Scholz y Maria Soria por ayudarme con la traducción y la revisión; es un placer trabajar cada día con estas personas increíbles en Lean Mind
Foto de Ivan Torres en Unsplash