1. Introducción
En este tutorial, echaremos un vistazo a DBUnit, una herramienta de prueba unitaria utilizada para probar las interacciones de bases de datos relacionales en Java.
Veremos cómo nos ayuda a llevar nuestra base de datos a un estado conocido y hacer valer contra un estado esperado.
2. Dependencias
Primero, podemos agregar DBUnit a nuestro proyecto desde Maven Central agregando la dependencia dbunit a nuestro pom.xml :
org.dbunit dbunit 2.7.0 test
Podemos buscar la versión más reciente en Maven Central.
3. Ejemplo de Hello World
A continuación, definamos un esquema de base de datos:
schema.sql :
CREATE TABLE IF NOT EXISTS CLIENTS ( `id` int AUTO_INCREMENT NOT NULL, `first_name` varchar(100) NOT NULL, `last_name` varchar(100) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE IF NOT EXISTS ITEMS ( `id` int AUTO_INCREMENT NOT NULL, `title` varchar(100) NOT NULL, `produced` date, `price` float, PRIMARY KEY (`id`) );
3.1. Definición del contenido inicial de la base de datos
DBUnit nos permite definir y cargar nuestro conjunto de datos de prueba de una manera declarativa simple .
Definimos cada fila de la tabla con un elemento XML, donde el nombre de la etiqueta es un nombre de la tabla, y los nombres y valores de los atributos se asignan a los nombres y valores de las columnas, respectivamente. Los datos de la fila se pueden crear para varias tablas. Tenemos que implementar el método getDataSet () de DataSourceBasedDBTestCase para definir el conjunto de datos inicial, donde podemos usar FlatXmlDataSetBuilder para referirnos a nuestro archivo XML:
data.xml :
3.2. Inicialización de la conexión y el esquema de la base de datos
Ahora que tenemos nuestro esquema, tenemos que inicializar nuestra base de datos.
Tenemos que extender la clase DataSourceBasedDBTestCase e inicializar el esquema de la base de datos en su método getDataSource () :
DataSourceDBUnitTest.java :
public class DataSourceDBUnitTest extends DataSourceBasedDBTestCase { @Override protected DataSource getDataSource() { JdbcDataSource dataSource = new JdbcDataSource(); dataSource.setURL( "jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;init=runscript from 'classpath:schema.sql'"); dataSource.setUser("sa"); dataSource.setPassword("sa"); return dataSource; } @Override protected IDataSet getDataSet() throws Exception { return new FlatXmlDataSetBuilder().build(getClass().getClassLoader() .getResourceAsStream("data.xml")); } }
Aquí, pasamos un archivo SQL a una base de datos H2 en memoria en su cadena de conexión. Si queremos probar en otras bases de datos, necesitaremos proporcionar nuestra implementación personalizada para ello.
Tenga en cuenta que , en nuestro ejemplo, DBUnit reinicializará la base de datos con los datos de prueba dados antes de la ejecución de cada método de prueba .
Hay varias formas de configurar esto a través de get SetUpOperation y obtener TearDownOperation :
@Override protected DatabaseOperation getSetUpOperation() { return DatabaseOperation.REFRESH; } @Override protected DatabaseOperation getTearDownOperation() { return DatabaseOperation.DELETE_ALL; }
La operación REFRESH le dice a DBUnit que actualice todos sus datos. Esto asegurará que se borren todas las cachés y que nuestra prueba unitaria no reciba influencia de otra prueba unitaria. La operación DELETE_ALL asegura que todos los datos se eliminen al final de cada prueba unitaria. En nuestro caso, le estamos diciendo a DBUnit que durante la configuración, usando la implementación del método getSetUpOperation actualizaremos todos los cachés. Finalmente, le decimos a DBUnit que elimine todos los datos durante la operación de desmontaje utilizando la implementación del método getTearDownOperation .
3.3. Comparación del estado esperado y el estado real
Ahora, examinemos nuestro caso de prueba real. Para esta primera prueba, lo haremos simple: cargaremos nuestro conjunto de datos esperado y lo compararemos con el conjunto de datos recuperado de nuestra conexión de base de datos:
@Test public void givenDataSetEmptySchema_whenDataSetCreated_thenTablesAreEqual() throws Exception { IDataSet expectedDataSet = getDataSet(); ITable expectedTable = expectedDataSet.getTable("CLIENTS"); IDataSet databaseDataSet = getConnection().createDataSet(); ITable actualTable = databaseDataSet.getTable("CLIENTS"); assertEquals(expectedTable, actualTable); }
4. Profundizar en las afirmaciones
En la sección anterior, vimos un ejemplo básico de comparar el contenido real de una tabla con un conjunto de datos esperado. Ahora vamos a descubrir el soporte de DBUnit para personalizar las afirmaciones de datos.
4.1. Afirmar con una consulta SQL
Una forma sencilla de verificar el estado real es con una consulta SQL .
En este ejemplo, insertaremos un nuevo registro en la tabla CLIENTES, luego verificaremos el contenido de la fila recién creada. Definimos la salida esperada en un archivo XML separado y extrajimos el valor real de la fila mediante una consulta SQL:
@Test public void givenDataSet_whenInsert_thenTableHasNewClient() throws Exception { try (InputStream is = getClass().getClassLoader().getResourceAsStream("dbunit/expected-user.xml")) { IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is); ITable expectedTable = expectedDataSet.getTable("CLIENTS"); Connection conn = getDataSource().getConnection(); conn.createStatement() .executeUpdate( "INSERT INTO CLIENTS (first_name, last_name) VALUES ('John', 'Jansen')"); ITable actualData = getConnection() .createQueryTable( "result_name", "SELECT * FROM CLIENTS WHERE last_name="Jansen""); assertEqualsIgnoreCols(expectedTable, actualData, new String[] { "id" }); } }
El método getConnection () de la clase ancestro DBTestCase devuelve una representación específica de DBUnit de la conexión de la fuente de datos (una instancia de IDatabaseConnection ). El método createQueryTable () de IDatabaseConnection se puede usar para obtener datos reales de la base de datos , para compararlos con el estado esperado de la base de datos, usando el método Assertion.assertEquals () . La consulta SQL pasada a createQueryTable () es la consulta que queremos probar. Devuelve una instancia de Table que usamos para hacer nuestra afirmación.
4.2. Ignorando columnas
A veces, en las pruebas de bases de datos, queremos ignorar algunas columnas de las tablas reales . Por lo general, estos son valores generados automáticamente que no podemos controlar estrictamente, como claves primarias generadas o marcas de tiempo actuales .
Podríamos hacer esto omitiendo las columnas de las cláusulas SELECT en las consultas SQL, pero DBUnit proporciona una utilidad más conveniente para lograrlo. Con los métodos estáticos de la clase DefaultColumnFilter podemos crear una nueva instancia de ITable a partir de una existente excluyendo algunas de las columnas , como se muestra aquí:
@Test public void givenDataSet_whenInsert_thenGetResultsAreStillEqualIfIgnoringColumnsWithDifferentProduced() throws Exception { Connection connection = tester.getConnection().getConnection(); String[] excludedColumns = { "id", "produced" }; try (InputStream is = getClass().getClassLoader() .getResourceAsStream("dbunit/expected-ignoring-registered_at.xml")) { IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is); ITable expectedTable = excludedColumnsTable(expectedDataSet.getTable("ITEMS"), excludedColumns); connection.createStatement() .executeUpdate("INSERT INTO ITEMS (title, price, produced) VALUES('Necklace', 199.99, now())"); IDataSet databaseDataSet = tester.getConnection().createDataSet(); ITable actualTable = excludedColumnsTable(databaseDataSet.getTable("ITEMS"), excludedColumns); assertEquals(expectedTable, actualTable); } }
4.3. Investigación de múltiples fallas
Si DBUnit encuentra un valor incorrecto, inmediatamente arroja un AssertionError .
In specific cases, we can use the DiffCollectingFailureHandler class, which we can pass to the Assertion.assertEquals() method as a third argument.
This failure handler will collect all failures instead of stopping on the first one, meaning that the Assertion.assertEquals() method will always succeed if we use the DiffCollectingFailureHandler. Therefore, we'll have to programmatically check if the handler found any errors:
@Test public void givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues() throws Exception { try (InputStream is = getClass().getClassLoader() .getResourceAsStream("dbunit/expected-multiple-failures.xml")) { IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is); ITable expectedTable = expectedDataSet.getTable("ITEMS"); Connection conn = getDataSource().getConnection(); DiffCollectingFailureHandler collectingHandler = new DiffCollectingFailureHandler(); conn.createStatement() .executeUpdate("INSERT INTO ITEMS (title, price) VALUES ('Battery', '1000000')"); ITable actualData = getConnection().createDataSet().getTable("ITEMS"); assertEquals(expectedTable, actualData, collectingHandler); if (!collectingHandler.getDiffList().isEmpty()) { String message = (String) collectingHandler.getDiffList() .stream() .map(d -> formatDifference((Difference) d)) .collect(joining("\n")); logger.error(() -> message); } } } private static String formatDifference(Difference diff) { return "expected value in " + diff.getExpectedTable() .getTableMetaData() .getTableName() + "." + diff.getColumnName() + " row " + diff.getRowIndex() + ":" + diff.getExpectedValue() + ", but was: " + diff.getActualValue(); }
Furthermore, the handler provides the failures in the form of Difference instances, which lets us format the errors.
After running the test we get a formatted report:
java.lang.AssertionError: expected value in ITEMS.price row 5:199.99, but was: 1000000.0 expected value in ITEMS.produced row 5:2019-03-23, but was: null expected value in ITEMS.title row 5:Necklace, but was: Battery at com.baeldung.dbunit.DataSourceDBUnitTest.givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues(DataSourceDBUnitTest.java:91)
Es importante notar que en este punto esperábamos que el nuevo artículo tuviera un precio de 199,99, pero era 1000000,0. Luego vemos que la fecha de producción será 2019-03-23, pero al final fue nula. Finalmente, el artículo esperado era un collar y en su lugar obtuvimos una batería.
5. Conclusión
En este artículo, vimos cómo DBUnit proporciona una forma declarativa de definir datos de prueba para probar capas de acceso a datos de aplicaciones Java.
Como siempre, el código fuente completo de los ejemplos está disponible en GitHub.