Tests de aprobación para bases de datos

Hace unos meses hice una introducción a los tests de aprobación en C# y acabé diciendo que aunque tenían sus limitaciones me parecían interesantes como herramienta a tener en cuenta en el futuro.

Recientemente he encontrado un caso en que los tests de aprobación me han resultado bastante útiles: testear procesos de integración con bases de datos.

Un escenario habitual

Hoy en día es difícil encontrar aplicaciones que funcionen de forma completamente aislada y aunque cada vez es más frecuente disponer de APIs de integración, muchas veces nos vemos obligados a tratar la base de datos como un punto de integración al que importar y exportar datos.

Escribir tests para verificar el comportamiento de estos procesos de importación y exportación no es una tarea agradable porque el estado observable del sistema es la propia base de datos, lo que hace que sea bastante incómodo testear el proceso usando los típicos Assert de NUnit u otros frameworks similares.

Sin embargo, es importante testear bien estos procesos porque muchas veces por motivo de eficiencia «se saltan» las capas intermedias de nuestra aplicación encargadas de realizar algunas validaciones y cálculos necesarios para generar la información en la base de datos.

Además no hay que olvidar que estamos permitiendo que otra aplicación genere la información sobre la que luego trabajará nuestro propio sistema, por lo que estamos aumentando la fragilidad de nuestro sistema y abriendo la puerta a posibles errores de los que luego nos tocará responder.

Usando tests de aprobación

Para los que no lo recuerden y no quieran leerse el anterior artículo sobre tests de aprobación, un test de aprobación se basa en generar algún tipo de salida (una imagen, una página web, un fichero de texto) y aprobarla manualmente, es decir, revisarla y decidir si es correcta o no. Si es correcta, quedará marcada como aprobada y el test seguirá pasando mientras no cambie la salida generada.

La diferencia fundamental con respecto a otros tipos de tests es que en lugar de decidir primero lo que debería pasar y luego comprobar si ha pasado, en un test de aprobación primero ejecutamos el código y luego vemos si el resultado nos parece adecuado o no.

En el caso de la base de datos, lo que haremos será lanzar el proceso de integración y comprobaremos el contenido de la base de datos después el proceso. Usando NUnit y Approval Tests podríamos escribir un test parecido a éste:

[Test]
public void Data_Is_Imported()
{
	// Creamos una base de datos limpia para partir de una situación
	// estable. Si estás usando un ORM, seguramente tengas herramientas
	// para hacer esto de una forma fácil, si no, puedes lanzar tus
	// scripts de creación de base de datos
	CreateEmptyDB();

	// Lanzamos el proceso de importación para cargar en la base de datos
	// los datos que llegarían de un sistema externo 
	ExecuteImportProcess("data_to_import.xml");

	// Obtenemos un string xml con el estado de las tablas que queremos 
	// verificar
	var dbState = GetTableContent("User", "Product", "Cateogry");

	// Aprobamos el estado
	Approvals.VerifyXml(dbState);
}

Los pasos son muy sencillos. Primero creamos una base de datos limpia para poder partir de una situación estable; si estás usando un ORM seguro que tienes formas de cargar el esquema de la BD, y si no, siempre puedes lanzar los scripts de creación de base de datos.

A continuación, lanzamos el proceso de importación sobre la base de datos que acabamos de crear y obtenemos el estado de las tablas que nos interesan. Lo más cómodo a la hora de analizar el resultado es generar un documento XML, pero podríamos hacerlo también con un CSV o cualquier otro formato de texto.

Por último, verificamos que el xml generado es correcto. La primera vez que ejecutemos el test no tendremos todavía ninguna salida aprobada por lo que el tests fallará. Approval Tests nos habrá dejado en la misma carpeta de la clase de test un fichero con el nombre clase.metodo.received.xml. Deberemos revisar este fichero y, si consideramos que su contenido es correcto, renombrarlo a clase.metodo.approved.xml. A partir de ese punto, cada vez que se ejecute el test se verificará que la salida obtenida es la misma que la salida aprobada, generando un error si no es así.

Cuando la aplicación vaya evolucionando y se produzcan cambios en el esquema de base de datos o en el propio proceso de integración, el test de aprobación fallará y nos veremos obligados a volver a revisar y aceptar el nuevo estado de la base de datos, lo que nos dará una oportunidad de comprobar si necesitamos hacer algo más para mantener el proceso de importación funcionando con los nuevos cambios.

Para obtener el estado de las tablas que nos interesan como XML hay varias alternativas, pero una forma muy sencilla y con un encantador toque retro es recurrir a los denostados DataSets sin tipo:

public string GetTableContent(params string[] tables)
{
	using (var conn = new SqlConnection("..."))
	{
		conn.Open();
		using (var command = conn.CreateCommand())
		using (var adapter = new SqlDataAdapter(command))
		{
			var dataset = new DataSet();

			foreach (var table in tables)
			{
				command.CommandText = string.Format("select * from [{0}]", table);
				adapter.Fill(dataset, table);
			}

			return dataset.GetXml();
		}
	}
}

Obviamente esta alternativa puede no ser válida si necesitamos probar procesos de importación con un número de datos muy elevados. En ese caso podríamos utilizar un IDataReader y generar un fichero CSV o exportar directammente el contenido de las tablas usando las herramientas propias de la base de datos, como Bulk Export.

Resumen

Los procesos de integración sobre bases de datos aparecen frecuentemente en muchas aplicaciones y son puntos críticos que pueden causarnos muchos problemas si no los tratamos con cariño.

Por sus características es complicado escribir buenos tests que comprueben su funcionamiento, pero usando tests de aprobación podemos conseguir un cierto grado de seguridad y, sobre todo, evitar que se introduzcan bugs de regresión cuando con el paso del tiempo se vaya modificando el esquema de la base de datos.