DBUnit – Exportar e Importar BBDD
0. Índice de contenidos.
- 1. Entorno.
- 2. Introducción
- 3. DBUnit-Definición
- 4. DBUnit-Componentes
- 5. Clase DBUnitUtils
- 6. Exportando BBDD
- 7. Importando BBDD
- 8. Borrando BBDD
- 9. Test JUnit
- 10.Conclusiones.
1. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil Dell Latitude E5500(Core Duo T9550 2.66GHz, 4GB RAM, 340 GB HD)
- Sistema operativo: Windows XP.
- Eclipse ganymede
- Maven 2.1.0
- JUnit 4.4
- Spring 2.5.6
- PostgreSQL 8.2
- Hsqldb 1.8.0.7
2. Introducción
Uno de los puntos importantes dentro de todo desarrollo debería de ser los test unitarios.
Con ellos podemos garantizar que nuestro código hace realmente lo que esperamos que haga y,
aunque parezca una obviedad, no siempre es así.
Para probar nuestras aplicaciones Java disponemos de un excelente framework como JUnit,que permite
evaluar si cada uno de los métodos de nuestras clases se comporta como se espera,es decir,en función de
los valores de entrada se evalua un retorno esperado.
Pero, ¿qué ocurre cuando nuestros test necesitan interactuar con una base de datos?,aquí es
donde entra en juego DBUnit proporcionándonos las herramientas necesarias para exportar
e importar nuestra base de datos de una manera sencilla.
3. DBUnit-Definicion
DBUnit es una extensión de JUnit para realizar test unitarios que permiten interactuar con un
conjunto de datos.
También permite dejar la base de datos en un
estado conocido antes y después de cada test, esto
con el fin de prevenir que datos corruptos
productos de test fallidos queden en la base de
datos ocasionando problemas a los test siguientes.
4. DBUnit-Componentes
Los componentes principales son:
- IDatabaseConnection. Interfaz que representa una conexión DBUnit a la base de datos.
- IDataSet. Interfaz que representa una colección de tablas (Manipula tablas y datos).
- DatabaseOperation. Clase abstracta que representa la operación que se va a realizar sobre la base de datos antes o después de un test.
De estos componentes cabe destacar:
IDataset:
- FlatXmlDataSet. Dataset leído y escrito como un documento XML planos.
- XmlDataSet. Dataset leído y escrito como un documento XML (DTD).
- DatabaseDataSet. Adaptador que provee acceso a la BD mediante una instancia de un dataset.
- QueryDataSet. Mantiene la colección de tablas resultado de una consulta a la BD en un dataset.
- DefaultDataSet. Usado para crear datasets de manera programada.
- XlsDataSet. Dataset leído y escrito.
DatabaseOperation:
- DatabaseOperation.UPDATE. Actualiza la BD con la información del dataset.
- DatabaseOperation.INSERT. Inserta en la BD el contenido del dataset.
- DatabaseOperation.DELETE. Elimina de la BD solamente los datos especificados en el dataset.
- DatabaseOperation.DELETE_ALL. Elimina todas las filas de las tablas especificadas en el dataset.
- DatabaseOperation.TRUNCATE. Hace un truncate de las tablas especificadas en el dataset.
- DatabaseOperation.REFRESH.Esta operación refresca el contenido del dataset en la BD, es decir, si es el caso actualiza o inserta nuevos registros.
- DatabaseOperation.CLEAN_INSERT. DELETE_ALL + INSERT.
- DatabaseOperation.NONE. No hace nada.
5. Clase DBUnitUtils
Esta es la clase de la que nos serviremos para exportar e importar nuestros juegos de datos con el fin de complementar
los test unitarios.A lo largo del tutorial explicaremos cada una de las partes de esta clase con el fin de
aprender el manejo basico de DBUnit.
package com.autentia.dbunit.test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import org.dbunit.database.DatabaseConnection; import org.dbunit.database.DatabaseSequenceFilter; import org.dbunit.database.IDatabaseConnection; import org.dbunit.database.QueryDataSet; import org.dbunit.dataset.FilteredDataSet; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.xml.FlatXmlDataSet; import org.dbunit.dataset.xml.FlatXmlWriter; import org.dbunit.operation.DatabaseOperation; public class DBUnitUtils { public static void generateXML(String driverName, String urlDB, String userDB, String passwordDB, String schemaBD, String nameXML) throws SQLException { Connection conn=null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passwordDB); IDatabaseConnection connection = new DatabaseConnection(conn, schemaBD); DatabaseSequenceFilter filter = new DatabaseSequenceFilter(connection); IDataSet datasetAll = new FilteredDataSet(filter, connection.createDataSet()); QueryDataSet partialDataSet = new QueryDataSet(connection); String[] listTableNames = filter.getTableNames(datasetAll); for (int i = 0; i < listTableNames.length; i++) { final String tableName = listTableNames[i]; // Specify the SQL to run to retrieve the data partialDataSet.addTable(tableName); } // Specify the location of the flat file(XML) FlatXmlWriter datasetWriter = new FlatXmlWriter(new FileOutputStream("C:\\" + nameXML + ".xml")); // Export the data datasetWriter.write(partialDataSet); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } public static void generatePartialXML(String driverName, String urlDB, String userDB, String passwordDB, String schemaBD, String nameXML) throws SQLException { Connection conn=null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passwordDB); IDatabaseConnection connection = new DatabaseConnection(conn, schemaBD); QueryDataSet partialDataSet = new QueryDataSet(connection); // Specify the SQL to run to retrieve the data partialDataSet.addTable("web_direccion"); partialDataSet.addTable("web_usuario"); // Specify the location of the flat file(XML) FlatXmlWriter datasetWriter = new FlatXmlWriter(new FileOutputStream("C:\\" + nameXML + ".xml")); // Export the data datasetWriter.write(partialDataSet); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } public static void createData(String driverName, String urlDB, String userDB, String passworDB, String nameXML) throws SQLException { Connection conn = null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passworDB); IDatabaseConnection connection = new DatabaseConnection(conn); DatabaseOperation.INSERT.execute(connection,new FlatXmlDataSet(new FileInputStream("C:\\" + nameXML + ".xml"))); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } public static void deleteData(String driverName, String urlDB, String userDB, String passworDB, String nameXML) throws SQLException { Connection conn = null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passworDB); IDatabaseConnection connection = new DatabaseConnection(conn); DatabaseOperation.DELETE.execute(connection, new FlatXmlDataSet(new FileInputStream("C:\\" + nameXML + ".xml"))); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } }
6. Exportando BBDD
En esta sección veremos como con DBUnit es posible generar un XML a partir de una base de datos existente de una
manera sencilla; pero antes hemos de tener en cuenta algunas consideraciones:
- Usar una instancia de base de datos por desarrollador.
-
Usar datasets pequeños (múltiples).
- Según la documentación oficial a partir de la versión 2.0 es posible usar datasets de gran tamaño pero en realidad el uso de grandes datasets provoca problemas de memoria.
-
Este punto es importante, pero la decisión dependerá en gran medida del volumen de datos que estemos manejando,es decir,si el volumen de datos es muy elevado
es muy aconsejable trabajar con dataset parciales; si no es así, podríamos trabajar con dataset completos sin problemas.
- Realizar la configuración de la carga de datos una sola vez por clase de test o test suite.
6.1 Exportación parcial
La implementación de este tipo de solución dependerá de la necesidad de cada momento.Por ejemplo:
- Necesitamos los datos de un par de tablas.Nuestro método quedaría algo como:
......... ........... public static void generatePartialXML(String driverName, String urlDB, String userDB, String passwordDB, String schemaBD, String nameXML) throws SQLException { Connection conn=null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passwordDB); IDatabaseConnection connection = new DatabaseConnection(conn, schemaBD); QueryDataSet partialDataSet = new QueryDataSet(connection); // Specify the SQL to run to retrieve the data partialDataSet.addTable("web_direccion"); partialDataSet.addTable("web_usuario"); // Specify the location of the flat file(XML) FlatXmlWriter datasetWriter = new FlatXmlWriter(new FileOutputStream("C:\\" + nameXML + ".xml")); // Export the data datasetWriter.write(partialDataSet); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } ......... ...........
Este es el caso más sencillo que nos podemos encontrar, ya que nuestro test necesita un
conjunto de datos obtenidos de un número reducido de tablas y, además, estas tablas no tienen relacción entre sí.
Sin embargo, debemos prestar especial atención a las relaciones entre las tablas que queremos exportar ya que es
fundamental mantener la integridad referencial de nuestra BBDD.Por ejemplo:
- Imaginemos que la tabla web_direccion tiene una FK con la tabla web_paises por el id_pais.En este caso
añadir las tablas al partialDataSet sería algo como:
......... ........... QueryDataSet partialDataSet = new QueryDataSet(connection); // Specify the SQL to run to retrieve the data partialDataSet.addTable("web_paises"); partialDataSet.addTable("web_direccion"); partialDataSet.addTable("web_usuario"); ......... ...........
Vemos que el orden de la insercción hace que se mantenga la integridad referencial,
ya que añadimos la tabla de web_pais antes que la tabla web_direccion, por lo que no habría ningún problema
a la hora de la importación; si lo hiciésemos al revés, añadiendo al dataset la tabla web_direccion antes
que la tabla web_paises, la importación desde el XML generado fallaría, ya que estaríamos intentando insertar
direcciones con paises que aún no existen en nuestra BBDD.
Otra ventaja de realizar exportaciones parciales es que tenemos la posibilidad de exportar
datos obtenidos a través de una consulta. Por ejemplo:
- Imaginemos que nuestro test solo necesita los usuarios cuyo id_usuario>5.
......... ........... QueryDataSet partialDataSet = new QueryDataSet(connection); // Specify the SQL to run to retrieve the data partialDataSet.addTable("web_paises"); partialDataSet.addTable("web_direccion"); partialDataSet.addTable("web_usuario","select * from web_usuario where id_usuario>5"); ......... ...........
6.2 Exportación total
Cuando hablamos de exportación total hemos de aclarar que no significa que no podamos utilizar QueryDataset,es más,
como veremos a continuación, trabajar con QueryDataset nos ayudará a mantener la integridad referencial de nuestra BBDD. La diferencia
respecto al punto anterior estriba en el número de tablas involucradas en la exportación. En una exportación total de la base
de datos es posible que haya un número considerable de tablas; por tanto, la opción anterior no sería recomendable, debido
a que mantener la integridad referencial de nuestra BBDD sería una tarea muy costosa.
A continuación se muestra cómo quedaría la implementación de una exportación total:
......... ........... public static void generateXML(String driverName, String urlDB, String userDB, String passwordDB, String schemaBD, String nameXML) throws SQLException { Connection conn=null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passwordDB); IDatabaseConnection connection = new DatabaseConnection(conn, schemaBD); DatabaseSequenceFilter filter = new DatabaseSequenceFilter(connection); IDataSet datasetAll = new FilteredDataSet(filter, connection.createDataSet()); QueryDataSet partialDataSet = new QueryDataSet(connection); String[] listTableNames = filter.getTableNames(datasetAll); for (int i = 0; i < listTableNames.length; i++) { final String tableName = listTableNames[i]; // Specify the SQL to run to retrieve the data partialDataSet.addTable(tableName); } // Specify the location of the flat file(XML) FlatXmlWriter datasetWriter = new FlatXmlWriter(new FileOutputStream("C:\\" + nameXML + ".xml")); // Export the data datasetWriter.write(partialDataSet); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } ......... ...........
Lo más importante a destacar es cómo DBUnit proporciona una herramienta
muy valiosa, como es la clase DatabaseSequenceFilter. A través de su método getTableNames(dataset)
podemos recuperar un array con los nombres de las tablas en el orden
correspondiente, según las relaciones entre las mismas.
Esto supone una gran ventaja, ya que si nuestra base de datos
tiene 200 tablas, con DatabaseSequenceFilter recuperamos el orden de insercción adecuado para generar
nuestro XML, y sabremos al 100 % que nuestra BBDD mantendrá la integridad referencial.
7. Importando BBDD.
Una opción muy extendida al realizar los test unitarios es crear una base de datos en memomria. En nuestro caso
será Hsqldb; por lo tanto, estaríamos exportando desde PostgreSQL e importando los datos en Hsqldb.El proceso de
importación es muy sencillo, si el XML generado con anterioridad es correcto.Lo vemos a continuación:
......... ........... public static void createData(String driverName, String urlDB, String userDB, String passworDB, String nameXML) throws SQLException { Connection conn = null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passworDB); IDatabaseConnection connection = new DatabaseConnection(conn); DatabaseOperation.INSERT.execute(connection,new FlatXmlDataSet(new FileInputStream("C:\\" + nameXML + ".xml"))); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } ......... ...........
Como podemos ver solo hace falta establecer la conexión con base de datos. A continuación, mediante el componente
DatabaseOperation indicamos qué operación queremos realizar; en este caso es la operación insertar. Es importatnte saber que
Hsqldb va creando la BBDD no solo a través de la información del XML, sino también de nuestras clases marcadas como entidades;
por lo que habrá que prestar atención a la definición de estas,ya que cualquier variación entre las tablas de BBDD y nuestras
clases marcadas como entidades, haría que la importación se fuese al traste.
8. Borrando BBDD
Una vez pasado el test unitario correspondiente, es una muy buena costumbre dejar la BBDD en la situación
que estaba antes de comenzar la prueba unitaria.Para ello hemos implementado el siguiente método,que es
exactamente igual que el de importación pero esta vez el componente DatabaseOperation nos servirá
para borrar la BBDD.Vemos la implementacion:
......... ........... public static void deleteData(String driverName, String urlDB, String userDB, String passworDB, String nameXML) throws SQLException { Connection conn = null; try { // Connect to the database DriverManager.registerDriver((Driver)Class.forName(driverName).newInstance()); conn = DriverManager.getConnection(urlDB, userDB, passworDB); IDatabaseConnection connection = new DatabaseConnection(conn); DatabaseOperation.DELETE.execute(connection, new FlatXmlDataSet(new FileInputStream("C:\\" + nameXML + ".xml"))); } catch (Exception exc) { exc.printStackTrace(); } finally{ conn.close(); } } ......... ...........
9. Test JUnit
Implementación de test unitario con JUnit:
package com.autentia.dbunit.test; import java.util.List; import javax.annotation.Resource; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.transaction.TransactionConfiguration; import org.springframework.transaction.annotation.Transactional; import com.autentia.dbunit.persistence.entity.WebUsuarios; import com.autentia.dbunit.persistence.entity.WebDirecciones; import com.autentia.dbunit.persistence.entity.WebPaises; import com.autentia.dbunit.persistence.Dao; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:applicationDBUniContext-test.xml" }) @TransactionConfiguration(transactionManager = "transacctionManager", defaultRollback = false) @Transactional public class DbUnitTest { @Resource private Dao dao; @BeforeClass public static void startUp() { try { DBUnitUtils.generateXML("org.postgresql.Driver", "jdbc:postgresql://localhost:5432/dbunit", "postgres", "postgres", "public", "dbUnit"); DBUnitUtils.createData("org.hsqldb.jdbcDriver", "jdbc:hsqldb:file:/tmp/appName/db/hsqldb/hibernate;shutdown=true", "sa", "", "dbUnit"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } @AfterClass public static void tearDown() { try { DBUnitUtils.deleteData("org.hsqldb.jdbcDriver", "jdbc:hsqldb:file:/tmp/appName/db/hsqldb/hibernate;shutdown=true", "sa", "", "dbUnit"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Test public void test1() { // Compruba la tabla de paises contenga datos Listpaises = dao.loadAll(WebPaises.class); Assert.assertTrue(paises != null); Assert.assertTrue(paises.size() > 0); // Compruba la tabla de direcciones contenga datos List direcciones = dao.loadAll(WebDirecciones.class); Assert.assertTrue(direcciones != null); Assert.assertTrue(direcciones.size() > 0); // Compruba la tabla de usuarios contenga datos List usuarios = dao.loadAll(WebUsuarios.class); Assert.assertTrue(usuarios != null); Assert.assertTrue(usuarios.size() > 0); } }
Como vemos, este test podría ser como cualquiera de los que ya hayamos realizado en alguna otra ocasión.Lo más importante a destacar
son los métodos startUp y tearDown anotados con @BeforeClass y @AfterClass repectivamente, lo que quiere decir que se ejecutarán antes
y después del test.Por tanto, antes de lanzar el test, generamos el XML y creamos la BBDD, importando los datos del XML generado. Después del
test borramos la BBDD dejándola en el estado inicial.
10. Conclusiones.
Siempre que nuestras pruebas unitarias necesiten un conjunto de datos con el que interactuar, DBUnit es una eficaz herramienta para llevar
a cabo esta misión.Sin embargo, no siempre es tan sencillo obtener un juego datos adecuado, ni siquiera con DBUnit,por lo que podemos decir que ha esta libreria todavía
le queda mucho por hacer en este sentido.
Para finalizar, me gustaría destacar una característica más sobre DBUnit, y es que podemos hacer test propios de BBDD,es decir,podemos comparar tablas,
columnas,tipo de datos etc .... entre distintas BBDD.Sobre esta cuestión profundizaremos en próximos tutoriales.
Un saludo.
Saul
Es una muy buena introducción. Especialmente productivo el tema de tener cuidado con tablas referenciales, y la exportación completa.
Puede ser productivo a la hora de hacer test, yo la utilizo a un esquema muy simple, que es el siguiente:
-Tengo una HSQLDB en la aplicación.
-Inicio los test unitarios, almacenando los valores anteriores en una BD.
-Ejecuto los test unitarios.
-Recupero los valores almacenados previamente por DBUnit.
Es una herramienta interesante, y seguro que un especialista en pruebas puede sacarle mucho más partido.
Saludos y gracias,
Jaime.
Un par de aportaciones:
-A nivel de trabajo en equipo, veo conveniente tener una BD propia (HSQLDB) y hacer una replica de la BD.
-A nivel de un proyecto individual, es suficiente con almacenar el estado de la BD y después de realizar los tests recuperar la copia de la BD antigua.
La elección, a gusto del consumidor. Posibilidades hay muchas con esta herramienta, la verdad. ¡La cuestión es saber elegir de manera acertada!
Consider using P6Spy
In as much as DBUnit works, it runs JDBC statements. If you’re having problems with DBUnit, P6Spy will show you what JDBC statements are actually being run by DBUnit. This has helped me more than a few times when I’ve forgotten a step.