diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7862b37bb..b651a6ad7 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -13,7 +13,7 @@
-null and will
* default to defaultValueComparer for all columns in all tables.
* @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
+ * @see #verifyData(ITable, ITable, String[], String[], ValueComparer, Map, boolean)
+ * to also control whether sorting considers only the filtered
+ * columns; this overload always sorts by all native columns.
*/
protected void verifyData(final ITable expectedTable,
final ITable actualTable, final String[] excludeColumns,
@@ -803,6 +817,52 @@ protected void verifyData(final ITable expectedTable,
final ValueComparer defaultValueComparer,
final Mapnull and
+ * will default.
+ * @param columnValueComparers
+ * {@link Map} of {@link ValueComparer}s to use for specific
+ * columns. Key is column name, value is the
+ * {@link ValueComparer}. Can be null and will
+ * default to defaultValueComparer for all columns in all tables.
+ * @param sortOnFilteredColumnsOnly
+ * True to sort the expected and actual tables by only the
+ * columns that survive excludeColumns/includeColumns, instead
+ * of by all of the actual table's native columns; see
+ * {@link VerifyTableDefinition#isSortOnFilteredColumnsOnly()}.
+ * @throws DatabaseUnitException if the tables' row counts, columns, or data do not match.
+ * @since 3.4.1
+ */
+ protected void verifyData(final ITable expectedTable,
+ final ITable actualTable, final String[] excludeColumns,
+ final String[] includeColumns,
+ final ValueComparer defaultValueComparer,
+ final Mapfalse, preserving the
+ * historical sort-by-all-columns behavior for backward compatibility.
+ *
+ * Set
+ * By default, {@link DefaultPrepAndExpectedTestCase#verifyData} sorts the
+ * actual table (loaded from the database) using all of its native columns,
+ * identity column first, but sorts the expected table (loaded from the
+ * expected dataset file) using only the columns present in that file - which
+ * excludes the identity column, since its value cannot be known ahead of
+ * time. Excluding the identity column from comparison via
+ * {@code excludeColumns} does not help: that filter is applied after both
+ * tables are already sorted, so it never influences the sort key.
+ *
+ * When production code inserts the rows in an order uncorrelated with their
+ * data - for example Hibernate reordering a batch insert - the database
+ * assigns identity values in that same uncorrelated order. The actual table
+ * then sorts by (arbitrary) insertion order while the expected table sorts by
+ * data content, so same-data rows compare against the wrong counterpart and
+ * the assertion fails despite both sides holding identical data.
+ *
+ * Setting {@code sortOnFilteredColumnsOnly} true makes both tables sort by
+ * only their filtered (post exclude/include) columns instead, so the
+ * identity column never participates in the sort and same-data rows line up
+ * regardless of insertion order.
+ *
+ * Tracked as GitHub issue #672 "PrepAndExpectedTestCase should only sort on
+ * filtered columns" (bug report), with companion feature request #676 "Allow
+ * PrepAndExpectedTestCase to sort only on filtered columns instead of all
+ * columns" - this class's fix.
+ *
+ * @author Jeff Jensen jeffjensen AT users.sourceforge.net
+ * @since 3.4.1
+ */
+class DefaultPrepAndExpectedTestCaseGeneratedIdRowOrderIT
+{
+ private static final String IDENTITY_TABLE_NAME = "IDENTITY_TABLE";
+ private static final String IDENTITY_TABLE_ID_COLUMN =
+ "IDENTITY_TABLE_ID";
+
+ private static final String PREP_DATA_FILE_NAME =
+ "/xml/generatedIdRowOrderPrep.xml";
+ private static final String EXPECTED_MATCH_DATA_FILE_NAME =
+ "/xml/generatedIdRowOrderExpectedMatch.xml";
+ private static final String EXPECTED_MISMATCH_DATA_FILE_NAME =
+ "/xml/generatedIdRowOrderExpectedMismatch.xml";
+
+ private final DataFileLoader dataFileLoader = new FlatXmlDataFileLoader();
+
+ @Test
+ void testVerifyData_defaultSortsAllColumns_rowsInsertedInDifferentOrderButSameData_throwsDbComparisonFailure()
+ throws Exception
+ {
+ final DefaultPrepAndExpectedTestCase tc = makeTestCase();
+ tc.configureTest(makeVerifyTableDefinitions(false),
+ new String[] {PREP_DATA_FILE_NAME},
+ new String[] {EXPECTED_MATCH_DATA_FILE_NAME});
+ tc.preTest();
+
+ // Documents the known defect described in the class Javadoc: both
+ // sides hold the same two rows, only their insertion/generated-ID
+ // order differs. The default (sortOnFilteredColumnsOnly=false)
+ // preserves this historical, backward-compatible behavior.
+ assertThatThrownBy(() -> tc.postTest())
+ .as("Expected the default sort-all-columns behavior to still"
+ + " reproduce the known generated-ID row order defect"
+ + " as a DbComparisonFailure; if this fails, the"
+ + " default may have changed - see this class's"
+ + " Javadoc.")
+ .isInstanceOf(DbComparisonFailure.class);
+ }
+
+ @Test
+ void testVerifyData_sortOnFilteredColumnsOnly_rowsInsertedInDifferentOrderButSameData_doesNotThrow()
+ throws Exception
+ {
+ final DefaultPrepAndExpectedTestCase tc = makeTestCase();
+ tc.configureTest(makeVerifyTableDefinitions(true),
+ new String[] {PREP_DATA_FILE_NAME},
+ new String[] {EXPECTED_MATCH_DATA_FILE_NAME});
+ tc.preTest();
+
+ // Opting in to sortOnFilteredColumnsOnly removes the identity column
+ // from the sort key on both sides, so same-data rows line up
+ // regardless of insertion order and the comparison passes.
+ assertThatCode(() -> tc.postTest())
+ .as("Expected sortOnFilteredColumnsOnly=true to fix the"
+ + " generated-ID row order defect so tc.postTest()"
+ + " does not throw, but it did.")
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void testVerifyData_sortOnFilteredColumnsOnly_rowsWithGenuineDataMismatch_throwsDbComparisonFailure()
+ throws Exception
+ {
+ final DefaultPrepAndExpectedTestCase tc = makeTestCase();
+ tc.configureTest(makeVerifyTableDefinitions(true),
+ new String[] {PREP_DATA_FILE_NAME},
+ new String[] {EXPECTED_MISMATCH_DATA_FILE_NAME});
+ tc.preTest();
+
+ // Control case: sortOnFilteredColumnsOnly=true must not mask a
+ // genuine data mismatch (not just a row order difference).
+ assertThatThrownBy(() -> tc.postTest())
+ .as("Expected tc.postTest() to throw DbComparisonFailure for"
+ + " genuinely mismatched data even with"
+ + " sortOnFilteredColumnsOnly=true, but it didn't.")
+ .isInstanceOf(DbComparisonFailure.class);
+ }
+
+ private VerifyTableDefinition[] makeVerifyTableDefinitions(
+ final boolean sortOnFilteredColumnsOnly)
+ {
+ final VerifyTableDefinition identityTable = new VerifyTableDefinition(
+ IDENTITY_TABLE_NAME, new String[] {IDENTITY_TABLE_ID_COLUMN});
+ identityTable
+ .setSortOnFilteredColumnsOnly(sortOnFilteredColumnsOnly);
+ return new VerifyTableDefinition[] {identityTable};
+ }
+
+ private DefaultPrepAndExpectedTestCase makeTestCase() throws Exception
+ {
+ return new DefaultPrepAndExpectedTestCase(dataFileLoader,
+ makeDatabaseTester());
+ }
+
+ private IDatabaseTester makeDatabaseTester() throws Exception
+ {
+ final DatabaseEnvironment dbEnv = DatabaseEnvironment.getInstance();
+ final IDatabaseConnection connection = dbEnv.getConnection();
+ return new DefaultDatabaseTester(connection);
+ }
+}
diff --git a/src/test/java/org/dbunit/DefaultPrepAndExpectedTestCaseTest.java b/src/test/java/org/dbunit/DefaultPrepAndExpectedTestCaseTest.java
index 5a45822f8..431bd65e9 100644
--- a/src/test/java/org/dbunit/DefaultPrepAndExpectedTestCaseTest.java
+++ b/src/test/java/org/dbunit/DefaultPrepAndExpectedTestCaseTest.java
@@ -6,6 +6,7 @@
import java.sql.Connection;
+import org.dbunit.assertion.DbComparisonFailure;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.MockDatabaseConnection;
@@ -401,6 +402,90 @@ void testVerifyData_withTwoTablesAndColumnFilters_passesWhenEqual()
.doesNotThrowAnyException();
}
+ @Test
+ void testVerifyData_withSortOnFilteredColumnsOnlyFalse_generatedIdOutOfDataOrder_throwsOnMismatch()
+ throws Exception
+ {
+ final Column[] actualColumns = {new Column("ID", DataType.INTEGER),
+ new Column("COL1", DataType.VARCHAR)};
+ final DefaultTable actualTable =
+ new DefaultTable("TEST_TABLE", actualColumns);
+ actualTable.addRow(new Object[] {2, "Apple"});
+ actualTable.addRow(new Object[] {1, "Banana"});
+
+ final Column[] expectedColumns = {new Column("COL1", DataType.VARCHAR)};
+ final DefaultTable expectedTable =
+ new DefaultTable("TEST_TABLE", expectedColumns);
+ expectedTable.addRow(new Object[] {"Apple"});
+ expectedTable.addRow(new Object[] {"Banana"});
+
+ final String[] excludeColumns = {"ID"};
+
+ final Throwable thrown = catchThrowable(() -> tc.verifyData(
+ expectedTable, actualTable, excludeColumns, null, null, null,
+ false));
+
+ assertThat(thrown)
+ .as("Default sortOnFilteredColumnsOnly=false must sort the"
+ + " actual table primarily by its excluded ID column,"
+ + " misaligning same-data rows and failing the"
+ + " comparison.")
+ .isInstanceOf(DbComparisonFailure.class);
+ }
+
+ @Test
+ void testVerifyData_withSortOnFilteredColumnsOnlyTrue_generatedIdOutOfDataOrder_passesWhenEqual()
+ throws Exception
+ {
+ final Column[] actualColumns = {new Column("ID", DataType.INTEGER),
+ new Column("COL1", DataType.VARCHAR)};
+ final DefaultTable actualTable =
+ new DefaultTable("TEST_TABLE", actualColumns);
+ actualTable.addRow(new Object[] {2, "Apple"});
+ actualTable.addRow(new Object[] {1, "Banana"});
+
+ final Column[] expectedColumns = {new Column("COL1", DataType.VARCHAR)};
+ final DefaultTable expectedTable =
+ new DefaultTable("TEST_TABLE", expectedColumns);
+ expectedTable.addRow(new Object[] {"Apple"});
+ expectedTable.addRow(new Object[] {"Banana"});
+
+ final String[] excludeColumns = {"ID"};
+
+ assertThatCode(() -> tc.verifyData(expectedTable, actualTable,
+ excludeColumns, null, null, null, true))
+ .as("sortOnFilteredColumnsOnly=true must sort both"
+ + " tables by only COL1, ignoring the excluded"
+ + " ID column, so same-data rows line up"
+ + " regardless of ID order.")
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void testVerifyData_withSortOnFilteredColumnsOnlyTrueAndAllColumnsExcluded_doesNotThrow()
+ throws Exception
+ {
+ final Column[] columns = {new Column("COL1", DataType.VARCHAR)};
+
+ final DefaultTable expectedTable =
+ new DefaultTable("TEST_TABLE", columns);
+ expectedTable.addRow(new Object[] {"expected-only"});
+
+ final DefaultTable actualTable = new DefaultTable("TEST_TABLE", columns);
+ actualTable.addRow(new Object[] {"actual-only"});
+
+ final String[] excludeColumns = {"COL1"};
+
+ // Excluding every column leaves makeSortColumns() with nothing to
+ // sort by; confirm SortedTable tolerates a zero-length sort-column
+ // list (a no-op, stable sort) instead of throwing.
+ assertThatCode(() -> tc.verifyData(expectedTable, actualTable,
+ excludeColumns, null, null, null, true))
+ .as("Excluding every column must not throw even"
+ + " though it leaves no columns to sort by.")
+ .doesNotThrowAnyException();
+ }
+
@Test
@TurkishDefaultLocale
void testVerifyData_withTurkishDefaultLocale_matchesAsciiIColumns()
diff --git a/src/test/java/org/dbunit/VerifyTableDefinitionTest.java b/src/test/java/org/dbunit/VerifyTableDefinitionTest.java
index 7d8167877..3f73be293 100644
--- a/src/test/java/org/dbunit/VerifyTableDefinitionTest.java
+++ b/src/test/java/org/dbunit/VerifyTableDefinitionTest.java
@@ -157,6 +157,75 @@ void testGetColumnValueComparers_whenNotSet_returnsNull()
.isNull();
}
+ @Test
+ void testConstructor_withoutSortOnFilteredColumnsOnly_defaultsFalse()
+ {
+ final VerifyTableDefinition def =
+ new VerifyTableDefinition("MY_TABLE", (String[]) null);
+ assertThat(def.isSortOnFilteredColumnsOnly())
+ .as("isSortOnFilteredColumnsOnly() should default to false when"
+ + " not set by the constructor.")
+ .isFalse();
+ }
+
+ @Test
+ void testConstructor_withSortOnFilteredColumnsOnlyTrue_storesSortOnFilteredColumnsOnly()
+ {
+ final VerifyTableDefinition def = new VerifyTableDefinition("ITEMS",
+ null, null, null, null, true);
+ assertThat(def.isSortOnFilteredColumnsOnly())
+ .as("isSortOnFilteredColumnsOnly() should return the value"
+ + " passed to the six-argument constructor.")
+ .isTrue();
+ }
+
+ @Test
+ void testConstructor_withExcludeColumnsComparersAndSortOnFilteredColumnsOnly_storesAllAndLeavesIncludeColumnsNull()
+ {
+ final String[] excluded = {"ID"};
+ final ValueComparer comparer = new IsActualEqualToExpectedValueComparer();
+ final Maptrue for tables whose first column - or any excluded
+ * column - is a generated/surrogate value not under the test's control
+ * (e.g. an identity column assigned in an insertion order production code
+ * does not guarantee, such as with Hibernate batch inserts): sorting by all
+ * columns then sorts the actual table primarily by that unpredictable value
+ * while the expected table usually sorts differently by its data content -
+ * misaligning same-data rows and failing the comparison.
+ *
+ * @since 3.4.1
+ */
+ private boolean sortOnFilteredColumnsOnly;
+
/**
* Create a valid instance with all columns compared except exclude the
* specified columns.
@@ -191,6 +209,86 @@ public VerifyTableDefinition(final String table,
final String[] excludeColumns, final String[] includeColumns,
final ValueComparer defaultValueComparer,
final Mapnull and
+ * will default.
+ * @param columnValueComparers
+ * {@link Map} of {@link ValueComparer}s to use for specific
+ * columns. Key is column name, value is {@link ValueComparer} to
+ * use for comparison of that column. Can be null
+ * and will default to defaultValueComparer for all columns in
+ * all tables.
+ * @param sortOnFilteredColumnsOnly
+ * True to sort the expected and actual tables by only this
+ * table's filtered columns instead of by all of the actual
+ * table's native columns. See {@link #sortOnFilteredColumnsOnly}.
+ * @since 3.4.1
+ */
+ public VerifyTableDefinition(final String table,
+ final String[] excludeColumns,
+ final ValueComparer defaultValueComparer,
+ final Mapnull and
+ * will default.
+ * @param columnValueComparers
+ * {@link Map} of {@link ValueComparer}s to use for specific
+ * columns. Key is column name, value is {@link ValueComparer} to
+ * use for comparison of that column. Can be null
+ * and will default to defaultValueComparer for all columns in
+ * all tables.
+ * @param sortOnFilteredColumnsOnly
+ * True to sort the expected and actual tables by only this
+ * table's filtered columns instead of by all of the actual
+ * table's native columns. See {@link #sortOnFilteredColumnsOnly}.
+ * @since 3.4.1
+ */
+ public VerifyTableDefinition(final String table,
+ final String[] excludeColumns, final String[] includeColumns,
+ final ValueComparer defaultValueComparer,
+ final Mapfalse.
+ *
+ * @see #sortOnFilteredColumnsOnly
+ *
+ * @param sortOnFilteredColumnsOnly
+ * True to sort by only its filtered columns, false to sort by
+ * all native columns.
+ * @since 3.4.1
+ */
+ public void setSortOnFilteredColumnsOnly(
+ final boolean sortOnFilteredColumnsOnly)
+ {
+ this.sortOnFilteredColumnsOnly = sortOnFilteredColumnsOnly;
+ }
}
diff --git a/src/site/asciidoc/components/verifytabledefinition.adoc b/src/site/asciidoc/components/verifytabledefinition.adoc
index a3f76cb2b..c074092fc 100644
--- a/src/site/asciidoc/components/verifytabledefinition.adoc
+++ b/src/site/asciidoc/components/verifytabledefinition.adoc
@@ -5,9 +5,10 @@
link:/dbunit/apidocs/org/dbunit/VerifyTableDefinition.html[VerifyTableDefinition]
(`org.dbunit`, since 2.4.8) defines one database table to verify: which columns to
-include or exclude from the comparison, and which
-link:../datacomparisons/valuecomparer.html[ValueComparer] (if any) to use per column.
-It is consumed by
+include or exclude from the comparison, which
+link:../datacomparisons/valuecomparer.html[ValueComparer] (if any) to use per column, and
+(<