Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>3.5.1-SNAPSHOT</version>
<version>3.6.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>dbUnit Extension</name>
<url>https://github.com/dbunit/dbunit-extension</url>
Expand Down
11 changes: 11 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
</properties>

<body>
<release version="3.6.0-SNAPSHOT" date="TBD" description="Row count check detecting test teardown missed or wrongly cleaned tables">
<action dev="jeffjensen" type="add" issue="939" system="github" due-to="jeffjensen">
Add RowCounter, QueryPerTableRowCounter, RowCountSnapshot, RowCountDifference, RowCountCheckConfiguration, RowCountCheck, RowCountChecker, and UnexpectedRowCountException to org.dbunit.database, plus DatabaseConfig.FEATURE_ROW_COUNT_CHECK, PROPERTY_ROW_COUNT_CHECK_EXCLUDE_TABLES, and PROPERTY_ROW_COUNTER: the core of an opt-in diagnostic that compares every table's row count before and after a test, failing the test when a count moved. Not yet wired into any test lifecycle.
</action>
<action dev="jeffjensen" type="add" issue="939" system="github" due-to="jeffjensen">
Wire the row count check into DefaultPrepAndExpectedTestCase: preTest() captures the baseline before setupData(), cleanupData() verifies it after the tear down operation, and postTest(false) discards it so a test that already failed does not also report a row count difference as noise. Add DefaultPrepAndExpectedTestCaseRowCountCheckIT covering a leaked row in an unlisted table, a reference table wrongly listed for cleanup, and the exclude list silencing either.
</action>
<action dev="jeffjensen" type="add" issue="939" system="github" due-to="jeffjensen">
Wire the row count check into DbUnitExtension: beforeTestExecution() captures the baseline before onSetup(), afterTestExecution() verifies it after onTearDown() unless the test method itself threw. Each capture/verify uses its own connection, acquired and closed independently of whatever connection onSetup()/onTearDown() use internally, and tolerates a null IDatabaseTester.getConnection() (e.g. a test double) by simply never activating for it.
</action>
</release>
<release version="3.5.0" date="Aug 11, 2026" description="A documentation site overhaul (new tutorials, 10 database vendor guides, a class-by-class Core Components reference, and a new Developing DbUnit contributor section); several new capabilities including DbUnitExtension for native JUnit 5/6 lifecycle management, a JSON dataset format with a matching JSON ValueComparer, MariaDB and H2 2.x database support, expanded PostgreSQL json/jsonb and array type support, and an opt-in escape hatch for schemas with circular foreign keys; and a broad set of correctness and performance fixes spanning metadata handling, DTD/XML export, PostgreSQL null-safety and large-object handling, and FlatXmlProducer memory use">
<action dev="jeffjensen" type="add" issue="840" system="github" due-to="jeffjensen">
Add repo-root README.adoc, rendered natively by GitHub via Asciidoctor, so the repository landing page shows a pitch, build/reproducible-build badges, a pointer to the "dbUnit in 5 Minutes" tutorial, and links to the documentation site, Maven coordinates, GitHub Discussions, and CONTRIBUTING.md instead of nothing.
Expand Down
90 changes: 90 additions & 0 deletions src/main/java/org/dbunit/DefaultPrepAndExpectedTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.dbunit.assertion.comparer.value.ValueComparer;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.rowcount.RowCountCheck;
import org.dbunit.database.rowcount.RowCountChecker;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.CompositeDataSet;
import org.dbunit.dataset.DataSetException;
Expand Down Expand Up @@ -132,6 +134,15 @@ public class DefaultPrepAndExpectedTestCase extends DBTestCase
*/
private Boolean cachedIsCaseSensitiveTableNames;

/**
* Manages the row count check baseline used by preTest() and cleanupData() to detect a
* table the test left dirty - either one it should have cleaned up and did not, or a
* reference table it wrongly cleaned.
*
* @since 3.6.0
*/
private final RowCountChecker rowCountChecker = new RowCountChecker();

private ExpectedDataSetAndVerifyTableDefinitionVerifier expectedDataSetAndVerifyTableDefinitionVerifier =
new DefaultExpectedDataSetAndVerifyTableDefinitionVerifier();

Expand Down Expand Up @@ -425,13 +436,40 @@ public void operationTearDownFinished(

/**
* {@inheritDoc}
* <p>
* Captures the row count check baseline, if enabled, before setting up the prep data - see
* {@link RowCountCheck#capture(IDatabaseConnection)}.
*/
@Override
public void preTest() throws Exception
{
captureRowCountBaseline();
setupData();
}

/**
* Capture the row count check baseline, using the connection shared with the rest of this
* test's lifecycle. A no-op that leaves no baseline captured when the check is disabled.
*
* @throws Exception On dbUnit errors.
* @since 3.6.0
*/
private void captureRowCountBaseline() throws Exception
{
final boolean acquiredConnectionHere = connection == null;
try
{
rowCountChecker.capture(getReusableConnection());
} catch (final Exception e)
{
if (acquiredConnectionHere)
{
closeReusableConnectionSuppressing(e);
}
throw e;
}
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -507,6 +545,11 @@ public void postTest() throws Exception

/**
* {@inheritDoc}
* <p>
* When {@code verifyData} is false - the test steps already failed - discards any
* captured row count check baseline, so cleanupData() skips that check too: the database
* is in an unknown state, so a count difference would be noise around the real failure,
* not a finding worth its own report.
*/
@Override
public void postTest(final boolean verifyData) throws Exception
Expand All @@ -517,6 +560,9 @@ public void postTest(final boolean verifyData) throws Exception
if (verifyData)
{
verifyData();
} else
{
rowCountChecker.discardBaseline();
}
} catch (final Throwable t)
{
Expand Down Expand Up @@ -585,6 +631,7 @@ public void cleanupData() throws Exception
makeReusableConnectionDatabaseTester(dataset);
reusableTester.onTearDown();
log.debug("cleanupData: Clean up done");
verifyRowCountUnchanged();
closeReusableConnection();
} catch (final Exception e)
{
Expand All @@ -594,6 +641,21 @@ public void cleanupData() throws Exception
}
}

/**
* Verify the row count check baseline, if one was captured, using the connection shared
* with the rest of this test's lifecycle. A no-op when no baseline was captured - the
* check is disabled, capture never ran, or {@link #postTest(boolean)} discarded it
* because the test steps already failed.
*
* @throws Exception On dbUnit errors, including {@link org.dbunit.database.rowcount.UnexpectedRowCountException}
* when a table's row count no longer matches the baseline.
* @since 3.6.0
*/
private void verifyRowCountUnchanged() throws Exception
{
rowCountChecker.verify(getReusableConnection());
}

/**
* Legacy JUnit-3-era tear-down hook. Not invoked automatically under JUnit 5;
* kept for subclasses that drive the lifecycle manually. Calling it after a
Expand Down Expand Up @@ -1488,6 +1550,34 @@ public void setFailureHandler(final FailureHandler failureHandler)
this.failureHandler = failureHandler;
}

/**
* Get the RowCountCheck in use.
*
* @see #rowCountChecker
*
* @return The RowCountCheck, or null if none has been resolved or set yet.
* @since 3.6.0
*/
public RowCountCheck getRowCountCheck()
{
return rowCountChecker.getRowCountCheck();
}

/**
* Set the RowCountCheck, overriding the one otherwise lazily built from the shared
* connection's DatabaseConfig.
*
* @see #rowCountChecker
*
* @param rowCountCheck
* The RowCountCheck to use.
* @since 3.6.0
*/
public void setRowCountCheck(final RowCountCheck rowCountCheck)
{
rowCountChecker.setRowCountCheck(rowCountCheck);
}

/**
* {@link IDatabaseTester} that runs setUp/tearDown operations against a
* connection supplied by the given {@link Callable} instead of calling
Expand Down
29 changes: 27 additions & 2 deletions src/main/java/org/dbunit/database/DatabaseConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.Properties;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.rowcount.QueryPerTableRowCounter;
import org.dbunit.database.rowcount.RowCounter;
import org.dbunit.database.statement.IStatementFactory;
import org.dbunit.database.statement.PreparedStatementFactory;
import org.dbunit.dataset.datatype.DefaultDataTypeFactory;
Expand Down Expand Up @@ -89,6 +91,12 @@ public class DatabaseConfig
/** Name of the property configuring the {@link IColumnFilter} used to determine MS SQL identity columns. */
public static final String PROPERTY_IDENTITY_COLUMN_FILTER =
"http://www.dbunit.org/properties/mssql/identityColumnFilter";
/** Name of the property configuring the table name patterns excluded from the row count check. */
public static final String PROPERTY_ROW_COUNT_CHECK_EXCLUDE_TABLES =
"http://www.dbunit.org/properties/rowCountCheckExcludeTables";
/** Name of the property configuring the {@link RowCounter} implementation to use. */
public static final String PROPERTY_ROW_COUNTER =
"http://www.dbunit.org/properties/rowCounter";

/** Name of the feature controlling whether table names are treated as case sensitive. */
public static final String FEATURE_CASE_SENSITIVE_TABLE_NAMES =
Expand Down Expand Up @@ -120,9 +128,15 @@ public class DatabaseConfig
*/
public static final String FEATURE_SKIP_CYCLE_CHECK =
"http://www.dbunit.org/features/skipCycleCheck";
/**
* Name of the feature controlling whether table row counts are compared before and after
* each test.
*/
public static final String FEATURE_ROW_COUNT_CHECK =
"http://www.dbunit.org/features/rowCountCheck";

/**
* A list of all properties as {@link ConfigProperty} objects.
* A list of all properties as {@link ConfigProperty} objects.
* The objects contain the allowed java type and whether or not a property is nullable.
*/
public static final ConfigProperty[] ALL_PROPERTIES = new ConfigProperty[] {
Expand All @@ -145,6 +159,9 @@ public class DatabaseConfig
new ConfigProperty(PROPERTY_ALLOW_VERIFYTABLEDEFINITION_EXPECTEDTABLE_COUNT_MISMATCH, Boolean.class, false),
new ConfigProperty(FEATURE_SORT_ALL_COLUMNS_WHEN_NO_PRIMARY_KEY, Boolean.class, false),
new ConfigProperty(FEATURE_SKIP_CYCLE_CHECK, Boolean.class, false),
new ConfigProperty(PROPERTY_ROW_COUNT_CHECK_EXCLUDE_TABLES, String[].class, false),
new ConfigProperty(PROPERTY_ROW_COUNTER, RowCounter.class, false),
new ConfigProperty(FEATURE_ROW_COUNT_CHECK, Boolean.class, false),
};

/**
Expand All @@ -159,7 +176,8 @@ public class DatabaseConfig
FEATURE_SKIP_ORACLE_RECYCLEBIN_TABLES,
FEATURE_ALLOW_EMPTY_FIELDS,
FEATURE_SORT_ALL_COLUMNS_WHEN_NO_PRIMARY_KEY,
FEATURE_SKIP_CYCLE_CHECK
FEATURE_SKIP_CYCLE_CHECK,
FEATURE_ROW_COUNT_CHECK
};

private static final DefaultDataTypeFactory DEFAULT_DATA_TYPE_FACTORY =
Expand All @@ -172,6 +190,9 @@ public class DatabaseConfig
private static final String[] DEFAULT_TABLE_TYPE = {"TABLE"};
private static final Integer DEFAULT_BATCH_SIZE = 100;
private static final Integer DEFAULT_FETCH_SIZE = 100;
private static final String[] DEFAULT_ROW_COUNT_CHECK_EXCLUDE_TABLES = new String[0];
private static final QueryPerTableRowCounter DEFAULT_ROW_COUNTER =
new QueryPerTableRowCounter();



Expand All @@ -192,6 +213,7 @@ public DatabaseConfig()
setFeature(FEATURE_ALLOW_EMPTY_FIELDS, false);
setFeature(FEATURE_SORT_ALL_COLUMNS_WHEN_NO_PRIMARY_KEY, false);
setFeature(FEATURE_SKIP_CYCLE_CHECK, false);
setFeature(FEATURE_ROW_COUNT_CHECK, false);

setProperty(PROPERTY_STATEMENT_FACTORY, PREPARED_STATEMENT_FACTORY);
setProperty(PROPERTY_RESULTSET_TABLE_FACTORY, RESULT_SET_TABLE_FACTORY);
Expand All @@ -204,6 +226,9 @@ public DatabaseConfig()
setProperty(
PROPERTY_ALLOW_VERIFYTABLEDEFINITION_EXPECTEDTABLE_COUNT_MISMATCH,
Boolean.FALSE);
setProperty(PROPERTY_ROW_COUNT_CHECK_EXCLUDE_TABLES,
DEFAULT_ROW_COUNT_CHECK_EXCLUDE_TABLES);
setProperty(PROPERTY_ROW_COUNTER, DEFAULT_ROW_COUNTER);

this.configurator = new Configurator(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
*
* The DbUnit Database Testing Framework
* Copyright (C)2002-2026, DbUnit.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package org.dbunit.database.rowcount;

import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;

/**
* {@link RowCounter} that issues one <code>SELECT COUNT(*)</code> per table, by
* looping {@link IDatabaseConnection#getRowCount(String)}. Already handles schema
* qualification and {@link DatabaseConfig#PROPERTY_ESCAPE_PATTERN} through
* {@link org.dbunit.util.QualifiedTableName}, and is exercised across every
* supported database by that method's own tests.
* <p>
* This is the {@link RowCountCheck} v1 implementation, and the value
* {@link DatabaseConfig#PROPERTY_ROW_COUNTER} initialises to.
*
* @author dbunit
* @since 3.6.0
*/
public class QueryPerTableRowCounter implements RowCounter
{
/**
* {@inheritDoc}
*/
@Override
public Map<String, Integer> countRows(final IDatabaseConnection connection,
final List<String> tableNames) throws SQLException
{
final Map<String, Integer> rowCounts = new LinkedHashMap<>();
for (final String tableName : tableNames)
{
rowCounts.put(tableName, connection.getRowCount(tableName));
}
return rowCounts;
}
}
Loading
Loading