1 /*
2 *
3 * The DbUnit Database Testing Framework
4 * Copyright (C)2002-2008, DbUnit.org
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 *
20 */
21 package org.dbunit.assertion;
22
23 import java.sql.SQLException;
24 import java.util.Arrays;
25
26 import org.dbunit.Assertion;
27 import org.dbunit.DatabaseUnitException;
28 import org.dbunit.database.IDatabaseConnection;
29 import org.dbunit.dataset.Column;
30 import org.dbunit.dataset.Columns;
31 import org.dbunit.dataset.DataSetException;
32 import org.dbunit.dataset.IDataSet;
33 import org.dbunit.dataset.ITable;
34 import org.dbunit.dataset.ITableMetaData;
35 import org.dbunit.dataset.datatype.DataType;
36 import org.dbunit.dataset.datatype.UnknownDataType;
37 import org.dbunit.dataset.filter.DefaultColumnFilter;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42 * Default implementation of DbUnit assertions, based on the original methods present
43 * at {@link Assertion}
44 *
45 * @author Felipe Leme (dbunit@felipeal.net)
46 * @author gommma (gommma AT users.sourceforge.net)
47 * @version $Revision$ $Date$
48 * @since 2.4.0
49 */
50 public class DbUnitAssert
51 {
52
53 /**
54 * Logger for this class
55 */
56 private static final Logger logger = LoggerFactory.getLogger(DbUnitAssert.class);
57
58 private FailureFactory junitFailureFactory = getJUnitFailureFactory();
59
60 /**
61 * Default constructor
62 */
63 public DbUnitAssert()
64 {
65 }
66
67 /**
68 * Compare one table present in two datasets ignoring specified columns.
69 *
70 * @param expectedDataset
71 * First dataset.
72 * @param actualDataset
73 * Second dataset.
74 * @param tableName
75 * Table name of the table to be compared.
76 * @param ignoreCols
77 * Columns to be ignored in comparison.
78 * @throws org.dbunit.DatabaseUnitException
79 * If an error occurs.
80 */
81 public void assertEqualsIgnoreCols(final IDataSet expectedDataset,
82 final IDataSet actualDataset, final String tableName,
83 final String[] ignoreCols) throws DatabaseUnitException
84 {
85 if (logger.isDebugEnabled())
86 logger.debug(
87 "assertEqualsIgnoreCols(expectedDataset={}, actualDataset={}, tableName={}, ignoreCols={}) - start",
88 new Object[] { expectedDataset, actualDataset, tableName,
89 Arrays.asList(ignoreCols) });
90
91 assertEqualsIgnoreCols(expectedDataset.getTable(tableName), actualDataset
92 .getTable(tableName), ignoreCols);
93 }
94
95 /**
96 * Compare the given tables ignoring specified columns.
97 *
98 * @param expectedTable
99 * First table.
100 * @param actualTable
101 * Second table.
102 * @param ignoreCols
103 * Columns to be ignored in comparison.
104 * @throws org.dbunit.DatabaseUnitException
105 * If an error occurs.
106 */
107 public void assertEqualsIgnoreCols(final ITable expectedTable,
108 final ITable actualTable, final String[] ignoreCols)
109 throws DatabaseUnitException
110 {
111 if (logger.isDebugEnabled())
112 logger
113 .debug(
114 "assertEqualsIgnoreCols(expectedTable={}, actualTable={}, ignoreCols={}) - start",
115 new Object[] {expectedTable, actualTable,
116 Arrays.asList(ignoreCols)});
117
118 final ITable expectedTableFiltered = DefaultColumnFilter
119 .excludedColumnsTable(expectedTable, ignoreCols);
120 final ITable actualTableFiltered = DefaultColumnFilter
121 .excludedColumnsTable(actualTable, ignoreCols);
122 assertEquals(expectedTableFiltered, actualTableFiltered);
123 }
124
125 /**
126 * Compare a table from a dataset with a table generated from an sql query.
127 *
128 * @param expectedDataset
129 * Dataset to retrieve the first table from.
130 * @param connection
131 * Connection to use for the SQL statement.
132 * @param sqlQuery
133 * SQL query that will build the data in returned second table rows.
134 * @param tableName
135 * Table name of the table to compare
136 * @param ignoreCols
137 * Columns to be ignored in comparison.
138 * @throws DatabaseUnitException
139 * If an error occurs while performing the comparison.
140 * @throws java.sql.SQLException
141 * If an SQL error occurs.
142 */
143 public void assertEqualsByQuery(final IDataSet expectedDataset,
144 final IDatabaseConnection connection, final String sqlQuery,
145 final String tableName, final String[] ignoreCols)
146 throws DatabaseUnitException, SQLException
147 {
148 if (logger.isDebugEnabled())
149 logger.debug(
150 "assertEqualsByQuery(expectedDataset={}, connection={}, tableName={}, sqlQuery={}, ignoreCols={}) - start",
151 new Object[] { expectedDataset, connection, tableName, sqlQuery,
152 ignoreCols });
153
154 ITable expectedTable = expectedDataset.getTable(tableName);
155 assertEqualsByQuery(expectedTable, connection, tableName, sqlQuery,
156 ignoreCols);
157 }
158
159 /**
160 * Compare a table with a table generated from an sql query.
161 *
162 * @param expectedTable
163 * Table containing all expected results.
164 * @param connection
165 * Connection to use for the SQL statement.
166 * @param tableName
167 * The name of the table to query from the database
168 * @param sqlQuery
169 * SQL query that will build the data in returned second table rows.
170 * @param ignoreCols
171 * Columns to be ignored in comparison.
172 * @throws DatabaseUnitException
173 * If an error occurs while performing the comparison.
174 * @throws java.sql.SQLException
175 * If an SQL error occurs.
176 */
177 public void assertEqualsByQuery(final ITable expectedTable,
178 final IDatabaseConnection connection, final String tableName,
179 final String sqlQuery, final String[] ignoreCols)
180 throws DatabaseUnitException, SQLException
181 {
182 if (logger.isDebugEnabled())
183 logger.debug(
184 "assertEqualsByQuery(expectedTable={}, connection={}, tableName={}, sqlQuery={}, ignoreCols={}) - start",
185 new Object[] { expectedTable, connection, tableName, sqlQuery,
186 ignoreCols });
187
188 ITable expected = DefaultColumnFilter.excludedColumnsTable(expectedTable,
189 ignoreCols);
190 ITable queriedTable = connection.createQueryTable(tableName, sqlQuery);
191 ITable actual = DefaultColumnFilter.excludedColumnsTable(queriedTable,
192 ignoreCols);
193 assertEquals(expected, actual);
194 }
195
196 /**
197 * Asserts that the two specified dataset are equals. This method ignore the
198 * tables order.
199 */
200 public void assertEquals(IDataSet expectedDataSet, IDataSet actualDataSet)
201 throws DatabaseUnitException
202 {
203 logger.debug("assertEquals(expectedDataSet={}, actualDataSet={}) - start",
204 expectedDataSet, actualDataSet);
205 assertEquals(expectedDataSet, actualDataSet, null);
206 }
207
208 /**
209 * Asserts that the two specified dataset are equals. This method ignore the
210 * tables order.
211 *
212 * @since 2.4
213 */
214 public void assertEquals(IDataSet expectedDataSet, IDataSet actualDataSet,
215 FailureHandler failureHandler) throws DatabaseUnitException
216 {
217 if (logger.isDebugEnabled())
218 logger.debug(
219 "assertEquals(expectedDataSet={}, actualDataSet={}, failureHandler={}) - start",
220 new Object[] { expectedDataSet, actualDataSet, failureHandler });
221
222 // do not continue if same instance
223 if (expectedDataSet == actualDataSet) {
224 return;
225 }
226
227 if (failureHandler == null) {
228 logger.debug("FailureHandler is null. Using default implementation");
229 failureHandler = getDefaultFailureHandler();
230 }
231
232 String[] expectedNames = getSortedUpperTableNames(expectedDataSet);
233 String[] actualNames = getSortedUpperTableNames(actualDataSet);
234
235 // tables count
236 if (expectedNames.length != actualNames.length) {
237 throw failureHandler.createFailure("table count", String
238 .valueOf(expectedNames.length), String.valueOf(actualNames.length));
239 }
240
241 // table names in no specific order
242 for (int i = 0; i < expectedNames.length; i++) {
243 if (!actualNames[i].equals(expectedNames[i])) {
244 throw failureHandler.createFailure("tables", Arrays.asList(
245 expectedNames).toString(), Arrays.asList(actualNames).toString());
246 }
247
248 }
249
250 // tables
251 for (int i = 0; i < expectedNames.length; i++) {
252 String name = expectedNames[i];
253 assertEquals(expectedDataSet.getTable(name), actualDataSet.getTable(name), failureHandler);
254 }
255
256 }
257
258 /**
259 * Asserts that the two specified tables are equals. This method ignores the
260 * table names, the columns order, the columns data type and which columns are
261 * composing the primary keys.
262 *
263 * @param expectedTable
264 * Table containing all expected results.
265 * @param actualTable
266 * Table containing all actual results.
267 * @throws DatabaseUnitException
268 */
269 public void assertEquals(ITable expectedTable, ITable actualTable)
270 throws DatabaseUnitException
271 {
272 logger.debug("assertEquals(expectedTable={}, actualTable={}) - start",
273 expectedTable, actualTable);
274 assertEquals(expectedTable, actualTable, (Column[]) null);
275 }
276
277 /**
278 * Asserts that the two specified tables are equals. This method ignores the
279 * table names, the columns order, the columns data type and which columns are
280 * composing the primary keys. <br />
281 * Example: <code><pre>
282 * ITable actualTable = ...;
283 * ITable expectedTable = ...;
284 * ITableMetaData metaData = actualTable.getTableMetaData();
285 * Column[] additionalInfoCols = Columns.getColumns(new String[] {"MY_PK_COLUMN"}, metaData.getColumns());
286 * assertEquals(expectedTable, actualTable, additionalInfoCols);
287 * </pre></code>
288 *
289 * @param expectedTable
290 * Table containing all expected results.
291 * @param actualTable
292 * Table containing all actual results.
293 * @param additionalColumnInfo
294 * The columns to be printed out if the assert fails because of a
295 * data mismatch. Provides some additional column values that may be
296 * useful to quickly identify the columns for which the mismatch
297 * occurred (for example a primary key column). Can be
298 * <code>null</code>
299 * @throws DatabaseUnitException
300 */
301 public void assertEquals(ITable expectedTable, ITable actualTable,
302 Column[] additionalColumnInfo) throws DatabaseUnitException
303 {
304 logger.debug(
305 "assertEquals(expectedTable={}, actualTable={}, additionalColumnInfo={}) - start",
306 new Object[] { expectedTable, actualTable, additionalColumnInfo });
307
308 FailureHandler failureHandler = null;
309 if (additionalColumnInfo != null)
310 failureHandler = getDefaultFailureHandler(additionalColumnInfo);
311
312 assertEquals(expectedTable, actualTable, failureHandler);
313 }
314
315 /**
316 * Asserts that the two specified tables are equals. This method ignores the
317 * table names, the columns order, the columns data type and which columns are
318 * composing the primary keys. <br />
319 * Example: <code><pre>
320 * ITable actualTable = ...;
321 * ITable expectedTable = ...;
322 * ITableMetaData metaData = actualTable.getTableMetaData();
323 * FailureHandler failureHandler = new DefaultFailureHandler();
324 * assertEquals(expectedTable, actualTable, failureHandler);
325 * </pre></code>
326 *
327 * @param expectedTable
328 * Table containing all expected results.
329 * @param actualTable
330 * Table containing all actual results.
331 * @param failureHandler
332 * The failure handler used if the assert fails because of a data
333 * mismatch. Provides some additional information that may be useful
334 * to quickly identify the rows for which the mismatch occurred (for
335 * example by printing an additional primary key column). Can be
336 * <code>null</code>
337 * @throws DatabaseUnitException
338 * @since 2.4
339 */
340 public void assertEquals(ITable expectedTable, ITable actualTable,
341 FailureHandler failureHandler) throws DatabaseUnitException
342 {
343 logger.trace("assertEquals(expectedTable, actualTable, failureHandler) - start");
344 logger.debug("assertEquals: expectedTable={}", expectedTable);
345 logger.debug("assertEquals: actualTable={}", actualTable);
346 logger.debug("assertEquals: failureHandler={}", failureHandler);
347
348 // Do not continue if same instance
349 if (expectedTable == actualTable) {
350 logger.debug(
351 "The given tables reference the same object. Will return immediately. (Table={})",
352 expectedTable);
353 return;
354 }
355
356 if (failureHandler == null) {
357 logger.debug("FailureHandler is null. Using default implementation");
358 failureHandler = getDefaultFailureHandler();
359 }
360
361 ITableMetaData expectedMetaData = expectedTable.getTableMetaData();
362 ITableMetaData actualMetaData = actualTable.getTableMetaData();
363 String expectedTableName = expectedMetaData.getTableName();
364
365 // Verify row count
366 int expectedRowsCount = expectedTable.getRowCount();
367 int actualRowsCount = actualTable.getRowCount();
368 if (expectedRowsCount != actualRowsCount) {
369 String msg = "row count (table=" + expectedTableName + ")";
370 Error error =
371 failureHandler.createFailure(msg, String
372 .valueOf(expectedRowsCount), String
373 .valueOf(actualRowsCount));
374 logger.error(error.toString());
375 throw error;
376 }
377 // if both tables are empty, it is not necessary to compare columns, as
378 // such
379 // comparison
380 // can fail if column metadata is different (which could occurs when
381 // comparing empty tables)
382 if (expectedRowsCount == 0 && actualRowsCount == 0) {
383 logger.debug("Tables are empty, hence equals.");
384 return;
385 }
386
387 // Put the columns into the same order
388 Column[] expectedColumns = Columns.getSortedColumns(expectedMetaData);
389 Column[] actualColumns = Columns.getSortedColumns(actualMetaData);
390
391 // Verify columns
392 Columns.ColumnDiff columnDiff =
393 Columns.getColumnDiff(expectedMetaData, actualMetaData);
394 if (columnDiff.hasDifference()) {
395 String message = columnDiff.getMessage();
396 Error error =
397 failureHandler.createFailure(message, Columns
398 .getColumnNamesAsString(expectedColumns), Columns
399 .getColumnNamesAsString(actualColumns));
400 logger.error(error.toString());
401 throw error;
402 }
403
404 // Get the datatypes to be used for comparing the sorted columns
405 ComparisonColumn[] comparisonCols = getComparisonColumns(expectedTableName,
406 expectedColumns, actualColumns, failureHandler);
407
408 // Finally compare the data
409 compareData(expectedTable, actualTable, comparisonCols, failureHandler);
410 }
411
412 /**
413 * @return The default failure handler
414 * @since 2.4
415 */
416 protected FailureHandler getDefaultFailureHandler()
417 {
418 return getDefaultFailureHandler(null);
419 }
420
421 /**
422 * @return The default failure handler
423 * @since 2.4
424 */
425 protected FailureHandler getDefaultFailureHandler(Column[] additionalColumnInfo)
426 {
427 DefaultFailureHandler failureHandler = new DefaultFailureHandler(additionalColumnInfo);
428 if (junitFailureFactory != null) {
429 failureHandler.setFailureFactory(junitFailureFactory);
430 }
431 return failureHandler;
432 }
433
434 /**
435 * @return the JUnitFailureFactory if JUnit is on the classpath or <code>null</code> if
436 * JUnit is not on the classpath.
437 */
438 private FailureFactory getJUnitFailureFactory()
439 {
440 try {
441 Class.forName("junit.framework.Assert");
442 // JUnit available
443 return new JUnitFailureFactory();
444 }
445 catch (ClassNotFoundException e) {
446 // JUnit not available on the classpath return null
447 logger.debug("JUnit does not seem to be on the classpath. " + e);
448 }
449 return null;
450 }
451
452 /**
453 * @param expectedTable
454 * Table containing all expected results.
455 * @param actualTable
456 * Table containing all actual results.
457 * @param comparisonCols
458 * The columns to be compared, also including the correct
459 * {@link DataType}s for comparison
460 * @param failureHandler
461 * The failure handler used if the assert fails because of a data
462 * mismatch. Provides some additional information that may be useful
463 * to quickly identify the rows for which the mismatch occurred (for
464 * example by printing an additional primary key column). Must not be
465 * <code>null</code> at this stage
466 * @throws DataSetException
467 * @since 2.4
468 */
469 protected void compareData(ITable expectedTable, ITable actualTable,
470 ComparisonColumn[] comparisonCols, FailureHandler failureHandler)
471 throws DataSetException
472 {
473 logger.debug("compareData(expectedTable={}, actualTable={}, "
474 + "comparisonCols={}, failureHandler={}) - start",
475 new Object[] {expectedTable, actualTable, comparisonCols,
476 failureHandler});
477
478 if (expectedTable == null) {
479 throw new NullPointerException(
480 "The parameter 'expectedTable' must not be null");
481 }
482 if (actualTable == null) {
483 throw new NullPointerException(
484 "The parameter 'actualTable' must not be null");
485 }
486 if (comparisonCols == null) {
487 throw new NullPointerException(
488 "The parameter 'comparisonCols' must not be null");
489 }
490 if (failureHandler == null) {
491 throw new NullPointerException(
492 "The parameter 'failureHandler' must not be null");
493 }
494
495 // iterate over all rows
496 for (int i = 0; i < expectedTable.getRowCount(); i++) {
497 // iterate over all columns of the current row
498 for (int j = 0; j < comparisonCols.length; j++) {
499 ComparisonColumn compareColumn = comparisonCols[j];
500
501 String columnName = compareColumn.getColumnName();
502 DataType dataType = compareColumn.getDataType();
503
504 Object expectedValue = expectedTable.getValue(i, columnName);
505 Object actualValue = actualTable.getValue(i, columnName);
506
507 // Compare the values
508 if (skipCompare(columnName, expectedValue, actualValue)) {
509 if (logger.isTraceEnabled()) {
510 logger.trace( "ignoring comparison " + expectedValue + "=" +
511 actualValue + " on column " + columnName);
512 }
513 continue;
514 }
515
516 if (dataType.compare(expectedValue, actualValue) != 0) {
517
518 Difference diff = new Difference(
519 expectedTable, actualTable,
520 i, columnName,
521 expectedValue, actualValue);
522
523 // Handle the difference (throw error immediately or something else)
524 failureHandler.handle(diff);
525 }
526 }
527 }
528
529 }
530
531 /**
532 * Method to last-minute intercept the comparison of a single
533 * expected and actual value. Designed to be overridden in order
534 * to skip cell comparison by specific cell values.
535 *
536 * @param columnName The column being compared
537 * @param expectedValue The expected value to be compared
538 * @param actualValue The actual value to be compared
539 * @return <code>false</code> always so that the comparison is never skipped
540 * @since 2.4
541 */
542 protected boolean skipCompare(String columnName, Object expectedValue, Object actualValue)
543 {
544 return false;
545 }
546
547 /**
548 * @param expectedTableName
549 * @param expectedColumns
550 * @param actualColumns
551 * @param failureHandler
552 * The {@link FailureHandler} to be used when no datatype can be
553 * determined
554 * @return The columns to be used for the assertion, including the correct
555 * datatype
556 * @since 2.4
557 */
558 protected ComparisonColumn[] getComparisonColumns(String expectedTableName,
559 Column[] expectedColumns, Column[] actualColumns,
560 FailureHandler failureHandler)
561 {
562 ComparisonColumn[] result = new ComparisonColumn[expectedColumns.length];
563
564 for (int j = 0; j < expectedColumns.length; j++) {
565 Column expectedColumn = expectedColumns[j];
566 Column actualColumn = actualColumns[j];
567 result[j] = new ComparisonColumn(expectedTableName, expectedColumn,
568 actualColumn, failureHandler);
569 }
570 return result;
571 }
572
573 protected String[] getSortedUpperTableNames(IDataSet dataSet)
574 throws DataSetException
575 {
576 logger.debug("getSortedUpperTableNames(dataSet={}) - start", dataSet);
577
578 String[] names = dataSet.getTableNames();
579 for (int i = 0; i < names.length; i++) {
580 names[i] = names[i].toUpperCase();
581 }
582 Arrays.sort(names);
583 return names;
584 }
585
586 /**
587 * Represents a single column to be used for the comparison of table data. It
588 * contains the {@link DataType} to be used for comparing the given column.
589 * This {@link DataType} matches the expected and actual column's datatype.
590 *
591 * @author gommma (gommma AT users.sourceforge.net)
592 * @author Last changed by: $Author: gommma $
593 * @version $Revision: 864 $ $Date: 2008-11-07 06:27:26 -0800 (Fri, 07 Nov
594 * 2008) $
595 * @since 2.4.0
596 */
597 public static class ComparisonColumn
598 {
599 /**
600 * Logger for this class
601 */
602 private static final Logger logger = LoggerFactory
603 .getLogger(ComparisonColumn.class);
604
605 private String columnName;
606 private DataType dataType;
607
608 /**
609 * @param tableName
610 * The table name which is only needed for debugging output
611 * @param expectedColumn
612 * The expected column needed to resolve the {@link DataType} to
613 * use for the actual comparison
614 * @param actualColumn
615 * The actual column needed to resolve the {@link DataType} to use
616 * for the actual comparison
617 * @param failureHandler
618 * The {@link FailureHandler} to be used when no datatype can be
619 * determined
620 */
621 public ComparisonColumn(String tableName, Column expectedColumn,
622 Column actualColumn, FailureHandler failureHandler) {
623 super();
624 this.columnName = expectedColumn.getColumnName();
625 this.dataType = getComparisonDataType(tableName, expectedColumn,
626 actualColumn, failureHandler);
627 }
628
629 /**
630 * @return The column actually being compared
631 */
632 public String getColumnName() {
633 return this.columnName;
634 }
635
636 /**
637 * @return The {@link DataType} to use for the actual comparison
638 */
639 public DataType getDataType() {
640 return this.dataType;
641 }
642
643 /**
644 * @param tableName
645 * The table name which is only needed for debugging output
646 * @param expectedColumn
647 * @param actualColumn
648 * @param failureHandler
649 * The {@link FailureHandler} to be used when no datatype can be
650 * determined
651 * @return The dbunit {@link DataType} to use for comparing the given
652 * column.
653 */
654 private DataType getComparisonDataType(String tableName,
655 Column expectedColumn, Column actualColumn,
656 FailureHandler failureHandler) {
657 if (logger.isDebugEnabled())
658 logger.debug(
659 "getComparisonDataType(tableName={}, expectedColumn={}, actualColumn={}, failureHandler={}) - start",
660 new Object[] { tableName, expectedColumn, actualColumn,
661 failureHandler });
662
663 DataType expectedDataType = expectedColumn.getDataType();
664 DataType actualDataType = actualColumn.getDataType();
665
666 // The two columns have different data type
667 if (!expectedDataType.getClass().isInstance(actualDataType)) {
668 // Expected column data type is unknown, use actual column data type
669 if (expectedDataType instanceof UnknownDataType) {
670 return actualDataType;
671 }
672
673 // Actual column data type is unknown, use expected column data type
674 if (actualDataType instanceof UnknownDataType) {
675 return expectedDataType;
676 }
677
678 // Impossible to determine which data type to use
679 String msg = "Incompatible data types: (table=" + tableName + ", col="
680 + expectedColumn.getColumnName() + ")";
681 throw failureHandler.createFailure(msg, String
682 .valueOf(expectedDataType), String.valueOf(actualDataType));
683 }
684
685 // Both columns have same data type, return any one of them
686 return expectedDataType;
687 }
688
689 }
690
691 }