Publica tu how-to

Dinos como hacer lo que sabes hacer, mándanos un email a wdonet@gmail.com y lo publicamos (dos días máximo) o si te interesa unirte al equipo de redactores, también háznoslo saber por correo.

DBUnit lo hace facil

Dado que he trabajado con JUnit y DBUnit y he encontrado en ellos unas buenas herramientas para probar el codigo de las aplicaciones en java, pues me dí a la tarea de investigar un poquito sobre como obtener datasets desde una BD, y como cargarlos para realizar pruebas específicas.  Fue entonces cuando me encontré con este artículo de Bill Siggelkow y me dí a la tarea de traducir una parte, también he agregado notas mías, pero pueden encontrar el original en enlace al final del post.

Y dice así:
Si desarrollas aplicaciones que acceden a datos relacionales necesitas verificar el código de tu aplicación trabaje con los datos, como fue diseñado.  Cuando usas Hibernate o algún otro framework que implemente JPA, deberías confirmar que tus mapeos dirijan a Hibernate para almacenar y recuperar la información correctamente.  Yo implemento este tipo de pruebas usando JUnit.  Nadie puede refutar que las pruebas contra una base de datos activa es fundamental - no hay una mejor forma para verificar tus mapeos.

Pero para probar contra la base de datos se requiere que los datos se encuentren en un estado inicial.  Puedes cargar la base de datos manualmente antes de correr tu prueba pero eso se vuelve monotono y propenso a errores. DBUnit resuelve este problema.  Este puede precargar la base de datos para cada una de tus pruebas, basado en un grupo de datos dentro de un xml (comúnmente llamado dataset).  El formato de este dataset es sencillo de entender; aquí hay un ejemplo de tres tablas de una base de datos MySQL.  Los nombres de los elementos empatan con los nombres de las tablas y sus atributos con los nombres de sus columnas. Cada elemento implica un registro en esa tabla.

<dataset>
    <cliente id_cliente="778899" id_estatus = "1" id_sucursal="98765"/>
    <banco id_banco="1" nombre="BNMX" descripcion="Banamex" activo="1"
valor="0"/>
    <pago id_cliente = "778899" id_estatus = "1" id_tipo_producto =
"1" id_promotor = "1" id_sucursal = "58" />
</dataset>

Para una base de datos pequeña donde solo necesitas cargar unas pocas tablas con unos pocos registros puedes crear el dataset manualmente. Pero seguramente no querás hacerlo con una base de datos empresarial compleja que contiene cientos de tablas con nombres de columnas complejas.  Es más facil si le permites a DbUnit extraer los datos por ti.



Aquí esta un proceso típico que sigues cuando usas DBUnit:

Poblar las tablas de tu base de datos como sea necesario para ejecutar tu prueba.
Extraer los datos desde esas tablas hacia un dataset XML. En el método setUp de tu prueba unitaria, llamar a DBUnit para cargar la base de datos desde los dataset's recientement creados.

Puedes extraer los datos usando una clase como esta:

import java.io.FileOutputStream;
import java.sql.Connection;
import java.sql.DriverManager;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;

public class ExtractTestDataSet {
    public static void main(String[] args) throws Exception {
        // conexion a la base de datos
        Class driverClass = Class.forName("com.mysql.jdbc.Driver");
        Connection jdbcConnection = DriverManager.getConnection(
                "jdbc:mysql://localhost/bon_app", "scott", "tiger");
        IDatabaseConnection connection = new DatabaseConnection(jdbcConnection);
        // exportar un dataset parcial
        QueryDataSet partialDataSet = new QueryDataSet(connection);
        partialDataSet.addTable("recipe", "SELECT * FROM recipe where
is_deleted != 'Y'");
        partialDataSet.addTable("recipe_ext_xref");
        FlatXmlDataSet.write(partialDataSet,
                new FileOutputStream("partial-dataset.xml"));
        // exportacion de la base de datos completa!!
        IDataSet fullDataSet = connection.createDataSet();
        FlatXmlDataSet.write(fullDataSet, new
FileOutputStream("full-dataset.xml"));
    }
}

DBUnit puede crear un dataset con todos los datos en tu base de datos configurándole solamente una conección JDBC abierta En la práctica, probablemente querá limitar el dataset a tablas específicas y datos usados por tu prueba unitaria. En el ejemplo anterior se esta creando dos archivos dataset. El primero como partial-dataset.xml, representa solo una porcion de la base de datos. DBUnit pobla el dataset basado en tablas y queries especificados. Si solo indicas el nombre de una tabla, el dataset se llenará de todos los registros y columnas en esa tabla. Especifia una tabla y un query y DBUnit solo extraerá los datos devueltos por el query. Si quieres la base de datos completa en el dataset, simplemente llama el metodo IDatabaseConnection.createDataSet().

En el método setUp() de tu prueba unitaria, usas el dataset como entrada para una operacion DBUnit. DBUnit soporta varias combinaciones de operacion para borrar, insetar y ctualizar la base de datos usando un dataset. Las operaciones más comunes son CLEAN_INSERT (limpia toda la tabla e inserta los registros del dataset) y REFRESH (solo agrega los registros del dataset a la tabla y si estos ya existen, los actualiza - con base en el id de la tabla). CLEAN_INSERT es la opcion a usar si tienes tu propia instancia de base de datos y no necesitas preocuparte por ponerle el pie a alguien más del equipo de desarrollo. Por otro lado, si compartes tu base de datos con el equipo, tu mejor amigo es REFRESH.

En el siguiente fragmento de una prueba unitaria, se está usando CLEAN_INSERT para reinicializar la base de datos antes de cada método de prueba.

protected void setUp() throws Exception {
    setUpDatabase();
}

private void setUpDatabase() throws Exception {
    // Inicializa tu base de datos aquí
    Class driverClass = Class.forName("com.mysql.jdbc.Driver");
    Connection jdbcConnection = DriverManager.getConnection(
"jdbc:mysql://localhost/bon_app", "scott", "tiger");
    IDatabaseConnection connection = new DatabaseConnection(jdbcConnection);

// inicializa tu dataset aquí
    IDataSet dataSet = new FlatXmlDataSet(new File("full-dataset.xml"));
    try {
        DatabaseOperation.CLEAN_INSERT.execute(connection, dataSet);
    } finally {
        connection.close();
    }
}

Esta forma trabaja bien pero es algo molesto que tengas que hacer código a mano y recompilar la clase que extrae cuando sea necesariocambiar como DBUnit crea los dataset. Usando Spring se le puede delegar el trabajo de configurar la clase de extracción de datos y esto salvaría un montón de código hecho a mano. Esto resulta en la siguiente clase:

package com.jadecove.bonappetit.test;
import java.io.FileOutputStream;
import java.sql.Connection;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;

import org.apache.log4j.Logger;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;

/**
* Extracts a DBUnit flat XML dataset from a database.
*
* @author Bill Siggelkow
*/
public class DBUnitDataExtractor {
    private DataSource dataSource;
    private String dataSetName = "dbunit-dataset.xml";
    private List queryList;
    private List tableList;
    private Map dbUnitProperties;
    private Map dbUnitFeatures;
    private String schema;

    /**
    * A regular expression that is used to get the table name
    * from a SQL 'select' statement.
    * This  pattern matches a string that starts with any characters,
    * followed by the case-insensitive word 'from',
    * followed by a table name of the form 'foo' or 'schema.foo',
    * followed by any number of remaining characters.
    */
    private static final Pattern TABLE_MATCH_PATTERN = Pattern.compile(".*\s+from\s+(\w+(\.\w+)?).*", Pattern.CASE_INSENSITIVE);
    private static final Logger log = Logger.getLogger(DBUnitDataExtractor.class);

    /**
    * The data source of the database from which the data will be
    extracted. This property
    * is required.
    *
    * @param ds
    */
    public void setDataSource(DataSource ds) {
        dataSource = ds;
    }

    /**
    * Set the schema.
    * @param schema
    */
    public void setSchema(String schema) {
        this.schema = schema;
    }

    /**
    * Name of the XML file that will be created. Defaults to
    <code>dbunit-dataset.xml</code>.
    *
    * @param name file name.
    */
    public void setDataSetName(String name) {
        dataSetName = name;
    }

    /**
    * Performs the extraction. If no tables or queries are specified,
    data from entire database
    * will be extracted. Otherwise, a partial extraction will be performed.
    * @throws Exception
    */
    public void extract() throws Exception {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            log.info("Beginning extraction from '"+conn.toString()+"'.");
            IDatabaseConnection connection = new DatabaseConnection(conn, schema);
            configConnection((DatabaseConnection) connection);
            if (tableList != null || queryList != null) {
                // partial database export
                QueryDataSet partialDataSet = new QueryDataSet(connection);
                addTables(partialDataSet);
                addQueries(partialDataSet);
                FlatXmlDataSet.write(partialDataSet, new FileOutputStream(dataSetName));
            } else {
                // full database export
                IDataSet fullDataSet = connection.createDataSet();
                FlatXmlDataSet.write(fullDataSet, new
                FileOutputStream(dataSetName));
            }
        }
        finally {
            if (conn != null) conn.close();
        }
        log.info("Completed extraction to '"+dataSetName+"'.");
    }

    /**
    * List of table names to extract data from.
    *
    * @param list of table names.
    */
    public void setTableList(List list) {
        tableList = list;
    }

    /**
    * List of SQL queries (i.e. 'select' statements) that will be used executed to retrieve
    * the data to be extracted. If the table being queried is also specified in the <code>tableList</code>
    * property, the query will be ignored and all rows will be extracted from that table.
    *
    * @param list of SQL queries.
    */
    public void setQueryList(List list) {
        queryList = list;
    }

    public void setDbUnitFeatures(Map dbUnitFeatures) {
        this.dbUnitFeatures = dbUnitFeatures;
    }

    public void setDbUnitProperties(Map dbUnitProperties) {
        this.dbUnitProperties = dbUnitProperties;
    }

    private void configConnection(DatabaseConnection conn) {
        DatabaseConfig config = conn.getConfig();
        if (dbUnitProperties != null) {
            for (Iterator k=dbUnitProperties.entrySet().iterator(); k.hasNext(); ) {
                Map.Entry entry = (Map.Entry) k.next();
                String name = (String) entry.getKey();
                Object value = entry.getValue();
                config.setProperty(name, value);
            }
        }
        if (dbUnitFeatures != null) {
            for (Iterator k=dbUnitFeatures.entrySet().iterator(); k.hasNext(); ) {
                Map.Entry entry = (Map.Entry) k.next();
                String name = (String) entry.getKey();
                boolean value = Boolean.valueOf((String)entry.getValue()).booleanValue();
                config.setFeature(name, value);
            }
        }
    }

    private void addTables(QueryDataSet dataSet) {
        if (tableList == null) return;
        for (Iterator k = tableList.iterator(); k.hasNext(); ) {
            String table = (String) k.next();
            dataSet.addTable(table);
        }
    }

    private void addQueries(QueryDataSet dataSet) {
        if (queryList == null) return;
        for (Iterator k = queryList.iterator(); k.hasNext(); ) {
            String query = (String) k.next();
            Matcher m = TABLE_MATCH_PATTERN.matcher(query);
            if (!m.matches()) {
                log.warn("Unable to parse query. Ignoring '" + query +"'.");
            }
            else {
                String table = m.group(1);
                // only add if the table has not been added
                if (tableList != null && tableList.contains(table)) {
                    log.warn("Table '"+table+"' already added. Ignoring '" + query + "'.");
                } else {
                    dataSet.addTable(table, query);
                }
            }
        }
    }
}

Esta clase sirve como una envoltura compatible con Spring sobre el proceso de extracción de DBUnit, aunque no tenga dependencia con Spring (eso es lo bonito de Spring). Aquí esta como puedes configurar el extractor en un archivo de beans de Spring.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property> <property name="url"><value>jdbc:mysql://localhost/bon_app</value></property> <property name="username"><value>scott</value></property> <property name="password"><value>tiger</value></property> </bean> <bean id="dbUnitDataExtractor" class="com.jadecove.bonappetit.test.DBUnitDataExtractor"> <property name="dataSource" ref="dataSource"/> <property name="dataSetName" value="dbunit-dataset.xml"/> <property name="tableList"> <list> <value>recipe</value> <value>recipe_ext_xref</value> </list> </property> <property name="queryList"> <list> <value>select * from bon_app_ext.person where first_name = 'Bill'</value> </list> </property> </bean> </beans>

Ahora puedes extraer datasets de DBUnit recuperando un extractor configurado vía Spring y llamando al método extract(). La única propiedad requerida es la fuente de datos. Haciendolo así puedes obtener la base de datos completa, pero también puedes listar tablas y queries específicos. Además, la configuración de Spring te permite agregar otros elementos y propiedades que configuren a DBUnit de diferentes maneras, revisa la API de DBUnit para saber qué más cosas puedes usar al respecto.

Cuando estas usando queries, DBUnit requiere que se especifique ambos, el nombre de la tabla y el query Para hacer una configuración más sencilla, este extractor usa una expresión regular que determina el nombre de la tabla.

Este extractor se ha usado con una base de datos empresarial compleja local MySQL, pero a través de la configuración de la conexión, esquema y características de DBUnit puedes extraer los datos desde la mayoría de bases de datos. En mi actual trabajo de consultoría, yo uso este extractor para obtener datos desde algunas bases de datos complejtas DB2 corriendo en un AS-400 de IBM.

Este ejercicio no solo ayudó a hacer la vida más fácil con DBUnit, esto me ayudó a entender que tan fácil es delegar estas tareas mundanas a Spring.

Fuente:
http://www.oreillynet.com/onjava/blog/2005/10/dbunit_made_easy.html

1 comentario:

Que opinas sobre esta publicación?