InjectedTestCaseConfigurer.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 java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.function.Supplier;
import org.dbunit.DefaultPrepAndExpectedTestCase;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.assertion.FailureHandler;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.util.fileloader.DataFileLoader;
import org.dbunit.util.fileloader.FileExtensionDataFileLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Pushes the {@code @DbUnitConfig}-driven values that reach an injected {@code @DbUnitTestCase}
* instance only through its own public API - the data file loader, the failure handler, the
* {@code @DbUnitProperty} values, {@code closeConnectionAfterTest}, and the
* {@code @DbUnitRowCountCheck} override - onto that instance, in a fixed order.
*
* <p>Only a value the test actually declares is pushed: an instance built with its own loader,
* failure handler, properties or {@code closeConnectionAfterTest=false} keeps them when
* {@code @DbUnitConfig} says nothing about them. The one exception is a missing data file
* loader, which an instance cannot load datasets without, so it is given the configured one.
* Declared properties are added to the instance's own rather than replacing them.
* {@code closeConnectionAfterTest} is declared only by setting it to {@code false}, since the
* default {@code true} is indistinguishable from not writing it. Each value pushed is recorded
* as an undo step, returned as an {@link InjectedTestCaseRestoration}, so a shared instance
* carries nothing from one method to the next.
*
* <p>Each setter first checks - via
* {@link DefaultMethodOverrideCheck#overridesDefaultMethod} - that the instance actually
* overrides it, since a non-overriding instance inherits {@link PrepAndExpectedTestCase}'s
* no-op default body and the configured value would otherwise silently never take effect. When
* a declared value cannot land that way, most setters fail fast;
* {@code setCloseConnectionAfterTest} only warns, because {@link AnnotatedTestExecutor} still
* honors that flag for its own connection regardless.
*
* <p>Not intended for direct use by test code; machinery consumed by {@link ExpectedLifecycle}.
*
* @author Jeff Jensen
* @since 3.6.0
*/
final class InjectedTestCaseConfigurer
{
private static final Logger log = LoggerFactory.getLogger(InjectedTestCaseConfigurer.class);
private final AnnotatedTestConfiguration configuration;
private final PrepAndExpectedTestCase testCase;
/**
* Creates a configurer for one injected test case.
*
* @param configuration The resolved configuration whose {@code @DbUnitConfig} values to
* apply.
* @param testCase The already-constructed {@code @DbUnitTestCase} instance to apply them to.
*/
InjectedTestCaseConfigurer(final AnnotatedTestConfiguration configuration,
final PrepAndExpectedTestCase testCase)
{
this.configuration = configuration;
this.testCase = testCase;
}
/**
* Applies every declared {@code @DbUnitConfig}-driven value to {@link #testCase}, in a fixed
* order: data file loader, failure handler, {@code DatabaseConfig} properties,
* close-connection flag, then row count check override.
*
* <p>If applying one value fails, the ones already applied are undone before the failure is
* thrown, so a failed attempt leaves the instance as it was found.
*
* @return The restoration that puts back what this applied; undoing it is a no-op when
* nothing was applied.
*/
InjectedTestCaseRestoration applyAll()
{
final List<Runnable> undoSteps = new ArrayList<>();
try
{
applyDataFileLoader(undoSteps);
applyFailureHandler(undoSteps);
applyDatabaseConfigProperties(undoSteps);
applyCloseConnectionAfterTest(undoSteps);
applyRowCountCheckOverride(undoSteps);
} catch (final RuntimeException failure)
{
InjectedTestCaseRestoration.undoSuppressing(
new InjectedTestCaseRestoration(undoSteps), failure);
throw failure;
}
return new InjectedTestCaseRestoration(undoSteps);
}
private void applyDataFileLoader(final List<Runnable> undoSteps)
{
final DataFileLoader dataFileLoader = configuration.getDataFileLoader();
final boolean declared = dataFileLoader.getClass() != FileExtensionDataFileLoader.class;
requireApplicable(declared, false, "setDataFileLoader",
new Class<?>[] {DataFileLoader.class},
() -> "DbUnitConfig.dataFileLoader() names "
+ dataFileLoader.getClass().getName(),
"it would silently keep loading with whatever loader it was already constructed"
+ " with. Override setDataFileLoader(), or drop dataFileLoader() from"
+ " @DbUnitConfig for this test.");
final boolean reportsLoader = reportsViaGetter("getDataFileLoader");
final DataFileLoader incoming = reportsLoader ? testCase.getDataFileLoader() : null;
if (!declared && (!reportsLoader || incoming != null))
{
return;
}
testCase.setDataFileLoader(dataFileLoader);
if (reportsLoader)
{
undoSteps.add(() -> testCase.setDataFileLoader(incoming));
} else
{
warnCannotRestore("getDataFileLoader", "data file loader");
}
}
private void applyFailureHandler(final List<Runnable> undoSteps)
{
final FailureHandler failureHandler = configuration.getFailureHandler();
final boolean declared = failureHandler != null;
requireApplicable(declared, false, "setFailureHandler",
new Class<?>[] {FailureHandler.class},
() -> "DbUnitConfig.failureHandler() names "
+ failureHandler.getClass().getName(),
"it would silently keep dbUnit's own default handler instead. Override"
+ " setFailureHandler(), or drop failureHandler() from @DbUnitConfig for"
+ " this test.");
if (!declared)
{
return;
}
final boolean reportsHandler = reportsViaGetter("getFailureHandler");
final FailureHandler incoming = reportsHandler ? testCase.getFailureHandler() : null;
testCase.setFailureHandler(failureHandler);
if (reportsHandler)
{
undoSteps.add(() -> testCase.setFailureHandler(incoming));
} else
{
warnCannotRestore("getFailureHandler", "failure handler");
}
}
private void applyDatabaseConfigProperties(final List<Runnable> undoSteps)
{
final Properties properties = configuration.getDatabaseConfigProperties();
final boolean declared = !properties.isEmpty();
requireApplicable(declared, false, "setDatabaseConfigProperties",
new Class<?>[] {Properties.class},
() -> "DbUnitConfig declares " + properties.size()
+ " @DbUnitProperty value(s)",
"this path never reaches the connection any other way - the setup/teardown"
+ " path's IOperationListener-based application never triggers here."
+ " Override setDatabaseConfigProperties(), or drop"
+ " properties()/propertiesProvider() from @DbUnitConfig for this"
+ " test.");
if (!declared)
{
// Setting the instance's own properties again is what puts back, on a connection it
// kept, the DatabaseConfig values it applied from them, when its lifecycle is
// abandoned before cleanupData() runs. An instance that cannot report them has no
// own properties to set, and setting null would wipe the ones it was built with.
if (reportsItsDatabaseConfigProperties())
{
final Properties ownProperties = testCase.getDatabaseConfigProperties();
undoSteps.add(() -> testCase.setDatabaseConfigProperties(ownProperties));
}
return;
}
warnIfOverridesSetUpDatabaseConfig(properties);
final Properties incoming = testCase.getDatabaseConfigProperties();
final Properties merged = new Properties();
if (incoming != null)
{
merged.putAll(incoming);
}
merged.putAll(properties);
testCase.setDatabaseConfigProperties(merged);
undoSteps.add(() -> testCase.setDatabaseConfigProperties(incoming));
}
private void applyCloseConnectionAfterTest(final List<Runnable> undoSteps)
{
final boolean declared = !configuration.isCloseConnectionAfterTest();
requireApplicable(declared, true,
"setCloseConnectionAfterTest", new Class<?>[] {boolean.class},
() -> "DbUnitConfig.closeConnectionAfterTest() is false",
"it may still close a connection this test's tester shares with other tests."
+ " This executor's own connection - for the row count check or"
+ " parameter injection - still honors the false value regardless.");
if (!declared)
{
return;
}
final boolean reportsFlag = reportsViaGetter("isCloseConnectionAfterTest");
final boolean incoming = !reportsFlag || testCase.isCloseConnectionAfterTest();
testCase.setCloseConnectionAfterTest(false);
if (reportsFlag)
{
undoSteps.add(() -> testCase.setCloseConnectionAfterTest(incoming));
} else if (DefaultMethodOverrideCheck.overridesDefaultMethod(testCase.getClass(),
PrepAndExpectedTestCase.class, "setCloseConnectionAfterTest", boolean.class))
{
warnCannotRestore("isCloseConnectionAfterTest", "closeConnectionAfterTest flag");
}
}
private void applyRowCountCheckOverride(final List<Runnable> undoSteps)
{
final boolean declared = configuration.isRowCountCheckDeclared();
requireApplicable(declared, false,
"setRowCountCheckOverride", new Class<?>[] {boolean.class, String[].class},
() -> "@DbUnitRowCountCheck is declared",
"the check would silently never run for this test. Override"
+ " setRowCountCheckOverride()/clearRowCountCheckOverride(), or drop"
+ " @DbUnitRowCountCheck for this test.");
if (!declared)
{
return;
}
testCase.setRowCountCheckOverride(configuration.isRowCountCheckEnabled(),
configuration.getRowCountCheckExclude());
undoSteps.add(testCase::clearRowCountCheckOverride);
}
/**
* Fails fast - or, when {@code warnOnly}, logs a warning - when {@code configured} is true
* but {@link #testCase}'s runtime type does not override {@code setterName}, inheriting
* {@link PrepAndExpectedTestCase}'s no-op default body so the configured value would
* silently never take effect. A no-op when nothing was configured for this setter, or it is
* genuinely overridden. See {@link DefaultMethodOverrideCheck#overridesDefaultMethod} for
* this check's own known limitation.
*
* @param configured Whether {@code @DbUnitConfig} configured a non-default value for this
* setter.
* @param warnOnly True to log a warning rather than throw - only
* {@code setCloseConnectionAfterTest}, which {@link AnnotatedTestExecutor} still
* honors for its own connection regardless.
* @param setterName The setter's name.
* @param setterParameterTypes The setter's parameter types.
* @param configuredClause What was configured, e.g.
* {@code "DbUnitConfig.failureHandler() names X"}, evaluated only when
* {@code configured} - it may read a value that is null otherwise.
* @param consequence What silently goes wrong when the value cannot land, and how to fix it.
*/
private void requireApplicable(final boolean configured, final boolean warnOnly,
final String setterName, final Class<?>[] setterParameterTypes,
final Supplier<String> configuredClause, final String consequence)
{
if (!configured
|| DefaultMethodOverrideCheck.overridesDefaultMethod(testCase.getClass(),
PrepAndExpectedTestCase.class, setterName, setterParameterTypes))
{
return;
}
final String message = configuredClause.get() + ", but the injected DbUnitTestCase "
+ testCase.getClass().getName() + " does not override " + setterName + "(); "
+ consequence;
if (warnOnly)
{
log.warn(message);
} else
{
throw new IllegalStateException(message);
}
}
/**
* Returns whether {@link #testCase}'s type overrides the named getter, so the value it
* reports is the instance's own and can be put back, rather than the interface default.
*
* @param getterName The name of a no-argument getter {@link PrepAndExpectedTestCase}
* declares as a default method.
* @return True when the instance's type overrides that getter.
*/
private boolean reportsViaGetter(final String getterName)
{
return DefaultMethodOverrideCheck.overridesDefaultMethod(testCase.getClass(),
PrepAndExpectedTestCase.class, getterName);
}
/**
* Warns that a value just applied cannot be put back after the test, because the instance
* does not report the one it replaced.
*
* @param getterName The getter the instance does not override.
* @param description What the value is, for the message.
*/
private void warnCannotRestore(final String getterName, final String description)
{
log.warn("The injected DbUnitTestCase {} does not override {}(), so the {} DbUnitConfig"
+ " declares stays on it after this test.", testCase.getClass().getName(),
getterName, description);
}
/**
* The extra {@code @DbUnitProperty} check {@link #applyDatabaseConfigProperties()} needs on
* top of the shared not-overridden check: a {@link DefaultPrepAndExpectedTestCase} subclass
* overriding {@code setUpDatabaseConfig(DatabaseConfig)} - the pre-3.6.0 way to configure a
* {@link DatabaseConfig} - drops the values just applied unless that override calls
* {@code super.setUpDatabaseConfig(config)}, since that same hook is where they would
* otherwise be applied. Reflection cannot tell whether the override calls {@code super},
* only that one exists, so this warns rather than fails. A no-op for a
* non-{@link DefaultPrepAndExpectedTestCase} implementation, whose own
* {@code setDatabaseConfigProperties()} override (already required by
* {@link #applyDatabaseConfigProperties()}) is its own business.
*
* @param properties The resolved {@code @DbUnitProperty} values; a no-op when empty.
*/
private void warnIfOverridesSetUpDatabaseConfig(final Properties properties)
{
if (properties.isEmpty() || !overridesSetUpDatabaseConfig())
{
return;
}
log.warn("DbUnitConfig declares {} @DbUnitProperty value(s) and the injected"
+ " DbUnitTestCase {} overrides setUpDatabaseConfig(DatabaseConfig); those"
+ " values reach the connection only if that override calls"
+ " super.setUpDatabaseConfig(config). Add the super call, or drop"
+ " properties()/propertiesProvider() and configure the DatabaseConfig"
+ " entirely in the override.", properties.size(),
testCase.getClass().getName());
}
/**
* Returns whether {@link #testCase} reports its own database configuration properties,
* that is, overrides both the properties getter and the properties setter, so they can be
* set again to put back what they applied.
*
* @return True when both the getter and the setter are overridden.
*/
private boolean reportsItsDatabaseConfigProperties()
{
final Class<?> type = testCase.getClass();
return DefaultMethodOverrideCheck.overridesDefaultMethod(type,
PrepAndExpectedTestCase.class, "getDatabaseConfigProperties")
&& DefaultMethodOverrideCheck.overridesDefaultMethod(type,
PrepAndExpectedTestCase.class, "setDatabaseConfigProperties",
Properties.class);
}
/**
* Returns whether {@link #testCase} is a {@link DefaultPrepAndExpectedTestCase} subclass
* that redeclares {@code setUpDatabaseConfig(DatabaseConfig)} somewhere below
* {@link DefaultPrepAndExpectedTestCase} itself.
*
* @return True when a {@link DefaultPrepAndExpectedTestCase} subclass overrides
* {@code setUpDatabaseConfig(DatabaseConfig)}.
*/
private boolean overridesSetUpDatabaseConfig()
{
return DefaultMethodOverrideCheck.declaresBelow(testCase.getClass(),
DefaultPrepAndExpectedTestCase.class, "setUpDatabaseConfig",
DatabaseConfig.class);
}
}