SetupTeardownLifecycle.java
/*
*
* 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.annotation.runtime;
import org.dbunit.IDatabaseTester;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.connection.TestScopedConnection;
import org.dbunit.dataset.CompositeDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.fileloader.DataFileLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The setup/teardown path of one test's dbUnit lifecycle, driven by
* {@link AnnotatedTestExecutor} when {@link AnnotatedTestConfiguration#isExpected()} is false:
* apply the {@code @DbUnitPrep} dataset (if any) and the {@code @DbUnitSetup}/
* {@code @DbUnitTearDown} operations to the tester, run its {@code onSetup()}/{@code onTearDown()},
* and around them capture and verify the {@link AnnotatedRowCountCheck} baseline.
*
* <p>Operations are applied only for a declared annotation: with no {@code @DbUnitPrep} and no
* {@code @DbUnitSetup}, {@code onSetup()} runs with whatever dataset and setup operation the
* tester already carries (e.g. from a {@code @BeforeEach} method); with no
* {@code @DbUnitTearDown}, {@code onTearDown()} runs with the tester's current teardown
* operation. The dataset and both operations {@link #tester} carries coming into the test are
* snapshotted at the start and restored at the end (see {@link TesterStateSnapshot}), so a
* tester shared across methods carries no per-method state onto the next method.
*
* @author Jeff Jensen
* @since 3.6.0
*/
final class SetupTeardownLifecycle
{
private static final Logger log = LoggerFactory.getLogger(SetupTeardownLifecycle.class);
private final AnnotatedTestConfiguration configuration;
private final IDatabaseTester tester;
private final TestScopedConnection connection;
private final AnnotatedRowCountCheck rowCountCheck;
private boolean reachedOnSetup;
private TesterStateSnapshot incomingState;
/**
* Creates the setup/teardown lifecycle for one test.
*
* @param configuration The resolved configuration.
* @param tester The tester to drive.
* @param connection The shared per-test connection holder.
* @param annotationDriven Whether the test opted into the {@code org.dbunit.annotation}
* family - false on the classic path, which never piggybacks the row count
* check baseline.
*/
SetupTeardownLifecycle(final AnnotatedTestConfiguration configuration,
final IDatabaseTester tester, final TestScopedConnection connection,
final boolean annotationDriven)
{
this.configuration = configuration;
this.tester = tester;
this.connection = connection;
this.rowCountCheck = new AnnotatedRowCountCheck(configuration, tester, connection,
annotationDriven);
}
/**
* Offers {@code retrieved} - a connection the tester's own {@code onSetup()}/
* {@code onTearDown()} just retrieved - to {@link AnnotatedRowCountCheck} for the
* piggybacked baseline capture. Called by {@link AnnotatedTestExecutor} from the installed
* {@link ExecutorOperationListener}.
*
* @param retrieved The connection the tester just retrieved.
*/
void adoptAndCaptureBaselineOnFirstConnection(final IDatabaseConnection retrieved)
{
rowCountCheck.adoptAndCaptureBaselineOnFirstConnection(retrieved);
}
/**
* Applies the prep dataset and setup operation, decides the row count check baseline, and
* runs {@code onSetup()}.
*
* @throws Exception If any step fails.
*/
void before() throws Exception
{
incomingState = TesterStateSnapshot.capture(tester);
final DatabaseOperation resolvedSetUpOperation = applySetUpOperation();
if (!rowCountCheck.canPiggybackBaselineOn(resolvedSetUpOperation))
{
rowCountCheck.captureBaselineEagerly();
}
applyPrepDataset();
reachedOnSetup = true;
tester.onSetup();
if (!rowCountCheck.isBaselineAttempted())
{
// The predicted piggyback never actually happened - e.g. a tester whose onSetup()
// does not end up calling executeOperation() the way AbstractDatabaseTester's does.
// Always leave a baseline decided one way or another before the test method runs,
// rather than silently skip the check because a prediction about a tester's
// internals turned out wrong. canPiggybackBaselineOn() only trusts a piggyback
// when onSetup() is AbstractDatabaseTester's own, so reaching here means that
// contract was not kept - the baseline captured now may already include the prep
// dataset's rows.
rowCountCheck.captureBaselineEagerly();
}
}
/**
* Resolves the setup operation {@code onSetup()} will run and applies it to the tester.
* Reads only the configuration and the tester's own current operation, never a connection,
* so {@link #before()} can settle the row count check baseline route - see
* {@link AnnotatedRowCountCheck#canPiggybackBaselineOn(DatabaseOperation)} - <em>before</em>
* {@link #applyPrepDataset()} runs: combining a multi-file {@code @DbUnitPrep} dataset
* resolves the shared connection to read its case-sensitivity feature, which would rule the
* piggyback out (and cost a fresh-connection tester an extra connection) if it ran first.
*
* @return The setup operation {@code onSetup()} will run.
*/
private DatabaseOperation applySetUpOperation()
{
if (configuration.getPrepDataFiles().length > 0 || configuration.isSetupDeclared())
{
// With @DbUnitSetup but no @DbUnitPrep, there is no dataset to apply the operation
// to, but the operation itself - most usefully NONE - still applies to whatever
// dataset the tester already has; see AnnotatedTestConfiguration#isSetupDeclared().
final DatabaseOperation setUpOperation = configuration.getSetUpOperation();
tester.setSetUpOperation(setUpOperation);
return setUpOperation;
}
log.debug("No @DbUnitPrep data files and no @DbUnitSetup declared; running with the"
+ " tester's current dataset and setup operation, restored afterward.");
return tester.getSetUpOperation();
}
/**
* Applies the {@code @DbUnitPrep} dataset to the tester: a single file loaded directly, or
* multiple files combined (see {@link #loadCombined}). With no {@code @DbUnitPrep} data
* files this only warns, when {@code @DbUnitSetup} declares a non-NONE operation with no
* dataset for it to act on - {@link #applySetUpOperation()} has already applied the
* operation itself.
*
* @throws Exception If loading or combining a prep dataset fails.
*/
private void applyPrepDataset() throws Exception
{
if (configuration.getPrepDataFiles().length > 0)
{
tester.setDataSet(loadCombined(configuration.getDataFileLoader(),
configuration.getPrepDataFiles()));
return;
}
if (configuration.isSetupDeclared())
{
final DatabaseOperation setUpOperation = configuration.getSetUpOperation();
warnIfNonNoneOperationHasNoDataset("@DbUnitSetup", "onSetup()", setUpOperation);
log.debug("No @DbUnitPrep data files declared; applied @DbUnitSetup's operation"
+ " {} without changing the tester's dataset.", setUpOperation);
}
}
/**
* Applies the teardown operation (when {@code @DbUnitTearDown} was declared), runs
* {@code onTearDown()}, then verifies the row count check baseline unless {@code testFailed}
* - all skipped when {@link #before()} threw before {@code onSetup()} ran, since
* {@code onTearDown()} would then act on state {@code onSetup()} never established and throw
* a second exception masking the first. Either way the incoming tester state is restored
* afterward - on a failure, with any restore failure attached as suppressed rather than
* masking the primary.
*
* @param testFailed Whether the test method itself already threw; when {@code true},
* verification is skipped so a difference does not mask the real failure.
* @throws Exception If any step fails.
*/
void after(final boolean testFailed) throws Exception
{
try
{
if (reachedOnSetup)
{
if (configuration.isTearDownDeclared())
{
final DatabaseOperation tearDownOperation =
configuration.getTearDownOperation();
tester.setTearDownOperation(tearDownOperation);
warnIfNonNoneOperationHasNoDataset("@DbUnitTearDown", "onTearDown()",
tearDownOperation);
} else
{
log.debug("No @DbUnitTearDown declared; running onTearDown() with the"
+ " tester's current teardown operation, restored afterward.");
}
tester.onTearDown();
if (!testFailed)
{
rowCountCheck.verify();
}
}
} catch (final Throwable primaryFailure)
{
TesterStateSnapshot.restoreSuppressing(tester, incomingState, primaryFailure);
throw primaryFailure;
}
TesterStateSnapshot.restore(tester, incomingState);
}
/**
* Warns when {@code annotationName} ({@code @DbUnitSetup} or {@code @DbUnitTearDown})
* declares a non-{@link DatabaseOperation#NONE} operation with no {@code @DbUnitPrep} data
* files and no dataset already on the tester - every such operation ({@code CLEAN_INSERT},
* {@code INSERT}, {@code REFRESH}, {@code UPDATE}, {@code DELETE}, {@code DELETE_ALL},
* {@code TRUNCATE_TABLE}) needs a dataset to act on, so {@code AbstractDatabaseTester}'s
* {@code testerMethodName} would run the operation against a {@code null} dataset. A bare
* {@code @DbUnitSetup} resolves to {@code CLEAN_INSERT}, so that is the likely shape of the
* mistake on the setup side; a bare {@code @DbUnitTearDown} resolves to {@code NONE}, so on
* the teardown side reaching this warning means the {@code operation} member was set
* explicitly.
*
* <p>A warning rather than an {@link IllegalStateException}: a custom {@link IDatabaseTester}
* whose {@code onSetup()}/{@code onTearDown()} sources its dataset some other way, or a
* test double, legitimately has {@code getDataSet()} return {@code null} here and handles
* the operation itself.
*
* @param annotationName The declaring annotation, {@code "@DbUnitSetup"} or
* {@code "@DbUnitTearDown"}.
* @param testerMethodName The tester lifecycle method that would run the operation,
* {@code "onSetup()"} or {@code "onTearDown()"}.
* @param resolvedOperation The operation the annotation resolved to.
*/
private void warnIfNonNoneOperationHasNoDataset(final String annotationName,
final String testerMethodName, final DatabaseOperation resolvedOperation)
{
if (resolvedOperation != DatabaseOperation.NONE && tester.getDataSet() == null)
{
log.warn("{} declares a non-NONE operation, but there is nothing for it to act on:"
+ " no @DbUnitPrep data files were declared, and the tester holds no"
+ " dataset. A built-in tester will fail in {}. Add @DbUnitPrep, change the"
+ " operation to DbUnitOperation.NONE, or set a dataset on the tester in a"
+ " @BeforeEach method.", annotationName, testerMethodName);
}
}
private IDataSet loadCombined(final DataFileLoader dataFileLoader, final String[] paths)
throws Exception
{
if (paths.length == 1)
{
return dataFileLoader.load(paths[0]);
}
final IDataSet[] dataSets = new IDataSet[paths.length];
for (int i = 0; i < paths.length; i++)
{
dataSets[i] = dataFileLoader.load(paths[i]);
}
try
{
return new CompositeDataSet(dataSets, true, isCaseSensitiveTableNames());
} catch (final DataSetException e)
{
throw new IllegalStateException("Failed to combine prep datasets.", e);
}
}
/**
* Returns whether an <em>already-resolved</em> connection reports
* {@code DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES} - the same feature
* {@code DefaultPrepAndExpectedTestCase#configureTest()} resolves for its own multi-file
* combining, so multiple {@code @DbUnitPrep} files combine the same way rather than always
* case-insensitively.
*
* <p>Reads only a connection {@link #before()} has already resolved
* ({@link TestScopedConnection#peekConnection()}); it never resolves one itself. On the row
* count check's piggyback route - see
* {@link AnnotatedRowCountCheck#canPiggybackBaselineOn(DatabaseOperation)} - no connection
* is resolved until {@code onSetup()} retrieves it, so this returns false and a multi-file
* prep dataset combines case-insensitively: the pre-annotation default, and identical to a
* case-sensitive combine unless two prep files hold tables whose names differ only in case.
* Resolving one here purely to read this feature would rule that route out and cost a
* fresh-connection tester (a plain {@code JdbcDatabaseTester}) a second physical connection
* every test. Every other route - a custom tester, a {@link DatabaseOperation#NONE} setup
* operation, a connection already in hand from {@code @BeforeEach} parameter injection -
* has resolved a connection before this runs, so the feature is honored.
*
* @return True when an already-resolved connection reports case-sensitive table names.
*/
private boolean isCaseSensitiveTableNames()
{
final IDatabaseConnection resolved = connection.peekConnection();
return resolved != null && resolved.getConfig()
.getFeature(DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES);
}
}