Conceptos básicos para testear una base de datos

Que me interesa el tema del testing es algo que, para cualquier lector de este blog, no es algo nuevo. En mi anterior post hablaba sobre la pirámide de los tests y veíamos que, dependendiendo del tipo de aplicación y de la arquitectura que tuviera, podía ser más interesante centrarse en un tipo de tests o en otro.

Hasta ahora he escrito bastante sobre cómo escribir tests unitarios (muy útiles cuando trabajas con un modelo de dominio rico), y también he comentado algunas técnicas para testear desde el interfaz de usuario con Canopy, pero si mi memoria (y google) no me falla, todavía no había escrito nada sobre cómo podemos testear una base datos.

En los próximos posts vamos a ver cómo podemos testear una base de datos y, como siempre, vamos a empezar por aclarar algunos conceptos básicos que debemos tener presentes si queremos escribir este tipo de tests.

¿Por qué testear contra la base de datos?

Aunque utilices un ORM (y yo lo sigo usando, aunque no esté de moda), es importante asegurarse de que el comportamiento de las operaciones que realizamos con la base de datos es el esperado.

Hay quien piensa que, con escribir tests unitarios sobre el modelo en memoria y utilizar mocks (o fakes o stubs o lo que más te guste) sobre la base de datos ya es suficiente, pero dependiendo del tipo de consulta que estemos lanzando a la base de datos, puede ser necesario tener tests específicos que trabajen contra la base de datos real para comprobar que todo es correcto.

Es cierto que este tipo de tests es más lento y, por tanto, se pierde parte de las ventajas de usar tests automatizados ya que el tiempo que tardamos en obtener feedback es mayor que en un test unitario, pero en general sigue siendo mucho más rápido que testear “a mano” usando el interfaz de usuario para crear los datos de ejemplo y revisar la base de datos para verificar que todo ha ido como esperábamos.

Este tipo de tests suele ser más complicado de escribir que un test unitario, especialmente por la preparación (setup) que requieren, pero con una estrategia adecuada y un poco de ayuda por parte de la infraestructura que montemos, esta limitación es salvable y podemos conseguir unos tests lo bastante claros y fáciles de escribir como para que merezca la pena hacerlo.

Principios básicos para testear una base de datos

Conceptualmente, testear una base de datos no es diferente de testear un modelo en memoria. La idea es exactamente la misma y se basa en el archiconocido arrange-act-assert (preparar, actuar, verificar).

Lo primero que necesitamos asegurar es que partimos siempre de un estado conocido. Uno de los problemas de testear una base de datos es que ésta no deja de ser un montón de estado global que puede ser modificado desde infinidad de puntos de la aplicación, por lo que necesitamos alguna forma de poder generar un estado inicial de forma cómoda (y lo más rápida posible) antes de ejecutar cada test.

Además, cada test necesitará su propio setup, lo que significa que tendremos que buscar una forma de preparar la base de datos con la información que necesitemos. Para ello, podemos utilizar el mismo API que usemos en nuestra aplicación para almacenar la información en la base de datos (ya sea un ORM, MicroORM, consultas a mano, etc.). De esta forma estaremos también testeando (en cierto modo) que esa parte de la aplicación funciona, lo que puede ser un beneficio añadido (aunque se podría argumentar que estamos acoplando dos conceptos distintos en los tests y que eso no es aconsejable).

Para esta parte de inicialización puede ser muy útil utilizar builders que nos ayuden a preparar la información y hagan el test más legible (y resistente), o incluso si tenemos un conjunto importante de datos maestros (por ejemplo una tabla de Países o de Formas de Pago), podemos emplear algo similar a un object mother para cargar esa información de referencia básica en la base de datos.

Una vez que tenemos montado nuestro escenario de test, el resto es fácil: invocar el método que lance la consulta real a la base de datos, y validar los resultados. Sobre esta parte no creo que haya mucho que contar porque es exactamente igual que en cualquier test que podamos escribir.

¿Cuáles son las partes complicadas?

En base a lo que acabamos de ver, podemos irnos haciendo una idea de las partes que más nos pueden costar al escribir este tipo de tests.

Puesto que tenemos que partir de un estado conocido, eso implica que necesitamos ser capaces de construir una base de datos sobre la que trabajar, y hemos de ser capaces de poder “limpiarla” antes de ejecutar cada test para asegurarnos de que sabemos de dónde partimos.

Lo normal es aprovechar en los tests el mismo sistema que usemos en producción para crear la base de datos desde cero. Si utilizas un ORM, normalmente tendrás la opción de dejar que sea él quien genere el esquema de datos. Si estás usando una aproximación más data centric, deberías tener un script sql para crear la base de datos.

Si no tienes una forma rápida y cómoda de crear una base de datos desde cero, no te preocupes por escribir este tipo de tests y empieza por solucionar ese problema, que es bastante más grave.

Una vez que tenemos creada la base de datos, necesitamos poder limpiarla entre tests. La opción más sencilla es volver a crearla entera antes de ejecutar cada tests, y si estás usando una base de datos que pueda funcionar en memoria (como SQLite, RavenDB o Datomic), puede ser suficiente. Lo malo es que esta técnica es la más lenta y, si tienes muchos tests, puede ser inviable.

Para mejorar la velocidad de los tests, se puede iniciar una transacción antes de cada test y realizar un rollback al final. Ésta es posiblemente la opción más rápida, pero si tu test va a utilizar sus propias transacciones, necesitas que la base de datos soporte transacciones anidadas, cosa que no todas las bases de datos hacen. Además, tendrás que tener en cuenta que de algunas cosas (como las secuencias en Oracle o los campos identity en SQLServer) no se hace un rollback real, por lo que tus tests no serían exactamente reproducibles. Que esto sea un problema o no, depende de cada caso concreto.

Otra opción es borrar las tablas que hayamos modificado durante el test, ya sea manualmente (lo que nos permite minimizar el número de consultas) o automáticamente (con algo parecido a esto).

Personalmente, suelo usar una opción mixta aprovechando los distintos puntos de inicialización que ofrece NUnit:

  • Creo la base de datos una única vez antes de ejecutar todos los tests usando un SetupFixture.
  • Borro todas las tablas de la base de datos y reseteo identies y secuencias antes de ejecutar cada clase de tests usando un TestFixtureSetup.
  • Borro las tablas concretas afectadas por los tests de cada fixture antes de ejecutar cada test usando un Setup.

Por último, es importante que cada máquina en que se vayan a ejecutar los tests (cada máquina de desarrollo y el servidor de integración continua, básicamente) cuente con su propia base de datos para ejecutar los tests sin interferir unos con otros.

Aquí estamos igual que antes, si no estabas haciendo eso ya, empieza por aquí, aunque nunca vayas a escribir tests contra la base de datos. Así evitarás problemas mientrás estás desarrollando y alguien introduce cambios en el esquema de datos.

Resumen

En este post hemos visto algunas ideas básicas que debemos tener en cuenta para escribir tests de integración contra la base de datos. Como siempre que empiezo una serie de posts, ha sido mucha teoría y poco código, pero en los próximos posts veremos como montar paso a paso una suite de tests de integración y comprobaremos que no es tan complicado como pudiera parecer al principio.


4 comentarios en “Conceptos básicos para testear una base de datos

  1. Muy de acuerdo con la mayoría de ideas que comentas, pero siempre he tenido mis dudas con respecto a “limpiar” la base de datos entre test y test. ¿Por qué es esto tan importante? Quiero decir, el código que estamos testeando va a ejecutarse en un sistema cuya de base de datos está cambiando constantemente. ¿Por qué simular que eso no ocurre en nuestro entorno de testing?
    En mi experiencia la ejecución del código no suele requerir algo así, sólo las aserciones que queremos realizar posteriormente sobre lo ejecutado. Y hasta ahora me ha resultado más útil repensar esas aserciones para hacerlas “resilientes” y no confiar en un determinado estado inicial.

  2. Hola Javier,

    Es interesante lo que comentas.

    Lo de limpiar la base de datos es para evitar que unos tests influyan en otros. Imagínate que tienes dos informes que acceden a las tablas Order/OrderLine, uno que calcula las ventas por producto en un rango de fechas, y otro las ventas por cliente.

    Para testear cada informe necesitarás insertar pedidos, y si no partes de una estructura conocida (y lo más fácil para eso es una base de datos vacía), los pedidos que insertes para los tests del informe de clientes pueden afectar a los tests del informe de productos.

    Una opción sería tener un conjunto de datos que te supieras “de memoria”, por ejemplo partir de la base de datos Northwind y tener hardcodeados los resultados esperados de cada informe, pero me parece que entonces el test queda más complicado de leer, porque para comprender lo que está pasando no me basta con mirar el código del test, sino que tengo que ir a la base de datos para ver cuál es su estado.

    Un saludo,

    Juanma

  3. Gracias por tu respuesta. Tiene mucho sentido lo que comentas y, como siempre, no hay “balas de plata”. En el escenario que describes, lo que intento hacer es buscar un elemento que me sirva para “discriminar” entre los tests y sus juegos de datos. Un candidato típico puede ser una fecha o una moneda (USD vs EUR, por ejemplo).
    En definitiva, no es algo sencillo ni transparente, pero creo que merece la pena por la comodidad de tener una sola base de datos que crece constantemente y que, indirectamente, se comporta de una forma más parecida al sistema real de lo que lo hará una base de datos que se crea cada vez desde cero.

  4. Pingback: Lo mejor de la semana sobre desarrollo web en español vol. 47 | ADWE

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>