TesterResolver.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.junit.jupiter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.dbunit.DatabaseTesterFactory;
import org.dbunit.IDatabaseTester;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.annotation.DbUnitTestCase;
import org.dbunit.annotation.DbUnitTester;
import org.dbunit.annotation.runtime.AnnotatedTestConfiguration;
import org.dbunit.annotation.runtime.DefaultMethodOverrideCheck;
import org.dbunit.annotation.runtime.ProvidedAttribute;
import org.dbunit.annotation.runtime.ReflectiveInstantiation;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.platform.commons.support.AnnotationSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resolves, for one test, the {@link IDatabaseTester} and (when present) the
* {@link PrepAndExpectedTestCase} instance {@link DbUnitExtension} drives - from a
* {@link DbUnitTestCase @DbUnitTestCase} field, a {@link DbUnitTester @DbUnitTester} field,
* {@link AnnotatedTestConfiguration#getDatabaseTesterFactory()}, or the 3.5.0 unmarked-field
* auto-scan, first match wins, walking every test instance in scope innermost first for
* {@code @Nested} support. See {@link DbUnitExtension}'s class Javadoc for the user-facing
* precedence contract.
*
* @author Jeff Jensen
* @since 3.6.0
*/
final class TesterResolver
{
private static final Logger log = LoggerFactory.getLogger(TesterResolver.class);
/**
* Resolves the tester and test case for the current test.
*
* @param context The extension context for the test method.
* @param configuration The resolved configuration (for
* {@code databaseTesterFactory()}).
* @return The resolved tester, and the test case when a {@code @DbUnitTestCase} field
* supplied one.
* @throws Exception If a field is null or the wrong type, a factory fails, or no tester can
* be found.
*/
Resolution resolve(final ExtensionContext context,
final AnnotatedTestConfiguration configuration) throws Exception
{
final Class<?> testClass = context.getRequiredTestClass();
final List<Object> instances = innermostFirst(context.getRequiredTestInstances());
final MarkedFields marked = findMarkedFields(instances);
final FieldMatch testCaseField = marked.testCaseField;
final FieldMatch testerField = marked.testerField;
if (testCaseField != null)
{
final Object testCaseFieldValue = testCaseField.value();
if (testCaseFieldValue == null)
{
throw new IllegalStateException("PrepAndExpectedTestCase field '"
+ testCaseField.field.getName() + "' in "
+ testCaseField.instance.getClass().getName() + " is null.");
}
if (!(testCaseFieldValue instanceof PrepAndExpectedTestCase))
{
throw new IllegalStateException("Field '" + testCaseField.field.getName()
+ "' in " + testCaseField.instance.getClass().getName()
+ " is annotated @DbUnitTestCase, but its value's type ("
+ testCaseFieldValue.getClass().getName()
+ ") does not implement PrepAndExpectedTestCase.");
}
final PrepAndExpectedTestCase testCase =
(PrepAndExpectedTestCase) testCaseFieldValue;
IDatabaseTester tester = testCase.getDatabaseTester();
if (tester == null)
{
// testCase does not override getDatabaseTester()/setDatabaseTester(), or does
// and was simply built without a tester yet (e.g. the no-arg-tester constructor
// form) - fall back to the same resolution a bare @DbUnitTestCase-less test
// would use, rather than leaving Resolution#tester null.
tester = findTester(instances, testClass, configuration);
// The executor drives this exact, already-injected instance directly (see
// AnnotatedTestExecutor#beforeExpectedTest()) rather than constructing a fresh
// one, so the fallback tester must be wired onto it here - otherwise it keeps
// whatever databaseTester it reports for an implementation that does not
// override setDatabaseTester().
testCase.setDatabaseTester(tester);
if (testCase.getDatabaseTester() != tester)
{
if (overridesSetDatabaseTester(testCase))
{
// Opted into automatic wiring by overriding setDatabaseTester(), so a
// round-trip failure means the override itself is broken - fail fast the
// same way every other @DbUnitConfig-driven setter does for a silent
// no-op, rather than let @DbUnitSetup/@DbUnitTearDown operations quietly
// target a tester this test case never actually uses.
throw new IllegalStateException("Field '" + testCaseField.field.getName()
+ "' in " + testCaseField.instance.getClass().getName()
+ " is annotated @DbUnitTestCase; its value's type ("
+ testCase.getClass().getName() + ") overrides"
+ " setDatabaseTester(), but getDatabaseTester() does not return"
+ " the same instance right after being given it."
+ " @DbUnitSetup/@DbUnitTearDown operations set on the resolved"
+ " IDatabaseTester would silently never reach the operations"
+ " this test case actually runs. Fix setDatabaseTester()/"
+ "getDatabaseTester() to round-trip the same instance.");
}
// A test case that never overrides setDatabaseTester() at all is the
// documented self-managed-connection pattern, not a bug - see
// annotations.adoc - so this stays a diagnostic log, not a failure.
log.debug("PrepAndExpectedTestCase {} does not round-trip"
+ " getDatabaseTester()/setDatabaseTester(); @DbUnitSetup/"
+ "@DbUnitTearDown operations set on the resolved IDatabaseTester"
+ " may not reach the operations this test case actually runs"
+ " unless it independently uses the same tester instance.",
testCase.getClass().getName());
}
}
return new Resolution(tester, testCase);
}
if (testerField != null)
{
final Object testerFieldValue = testerField.value();
if (testerFieldValue == null)
{
throw new IllegalStateException("IDatabaseTester field '"
+ testerField.field.getName() + "' in "
+ testerField.instance.getClass().getName() + " is null.");
}
if (!(testerFieldValue instanceof IDatabaseTester))
{
throw new IllegalStateException("Field '" + testerField.field.getName() + "' in "
+ testerField.instance.getClass().getName()
+ " is annotated @DbUnitTester, but its value's type ("
+ testerFieldValue.getClass().getName()
+ ") does not implement IDatabaseTester.");
}
return new Resolution((IDatabaseTester) testerFieldValue, null);
}
return new Resolution(findTester(instances, testClass, configuration), null);
}
/**
* Returns whether {@code testCase}'s runtime type overrides
* {@link PrepAndExpectedTestCase#setDatabaseTester(IDatabaseTester)}, rather than
* inheriting the interface's own no-op default body - the same distinction
* {@code org.dbunit.annotation.runtime.InjectedTestCaseConfigurer} makes for its
* {@code @DbUnitConfig}-driven setters. Used by
* {@link #resolve(ExtensionContext, AnnotatedTestConfiguration)} to tell a
* {@link PrepAndExpectedTestCase} that deliberately manages its own tester - never
* overriding this method, a documented and supported pattern - apart from one that opted
* into automatic wiring by overriding it, but whose override does not actually work. See
* {@link DefaultMethodOverrideCheck#overridesDefaultMethod} for this check's own known
* limitation.
*
* @param testCase The instance to check.
* @return True when {@code testCase}'s type overrides {@code setDatabaseTester()}.
*/
private boolean overridesSetDatabaseTester(final PrepAndExpectedTestCase testCase)
{
return DefaultMethodOverrideCheck.overridesDefaultMethod(testCase.getClass(),
PrepAndExpectedTestCase.class, "setDatabaseTester", IDatabaseTester.class);
}
private List<Object> innermostFirst(final TestInstances instances)
{
final List<Object> all = new ArrayList<>(instances.getAllInstances());
Collections.reverse(all);
return all;
}
private IDatabaseTester findTester(final List<Object> instances, final Class<?> testClass,
final AnnotatedTestConfiguration configuration) throws Exception
{
if (configuration.getDatabaseTesterFactory() != null)
{
final DatabaseTesterFactory factory = ReflectiveInstantiation.instantiate(
configuration.getDatabaseTesterFactory(),
"DbUnitConfig.databaseTesterFactory");
return ProvidedAttribute.requireProvided(factory.getDatabaseTester(),
DatabaseTesterFactory.class.getSimpleName(),
configuration.getDatabaseTesterFactory(),
"DbUnitConfig.databaseTesterFactory", "getDatabaseTester");
}
return autoScanTesterField(instances, testClass);
}
/**
* Finds the innermost test instance declaring a {@link DbUnitTestCase} or
* {@link DbUnitTester} field, checking both markers together on each instance in turn - the
* same innermost-instance-wins precedent {@link #autoScanTesterField} already establishes
* for the plain, unannotated field - so a match on one instance shadows an outer instance's
* marked field entirely, regardless of whether the outer field uses the same marker or the
* other one, rather than the two being resolved independently and compared across instances.
*
* @throws IllegalStateException If one instance declares both markers, or more than one
* field for the same marker within one instance's class hierarchy.
*/
private MarkedFields findMarkedFields(final List<Object> instances)
{
for (final Object instance : instances)
{
final FieldMatch testCaseField = findMarkedField(instance, DbUnitTestCase.class);
final FieldMatch testerField = findMarkedField(instance, DbUnitTester.class);
if (testCaseField != null && testerField != null)
{
throw new IllegalStateException(
"Both @DbUnitTestCase and @DbUnitTester fields are declared in "
+ instance.getClass().getName() + "; declare at most one.");
}
if (testCaseField != null || testerField != null)
{
return new MarkedFields(testCaseField, testerField);
}
}
return new MarkedFields(null, null);
}
/**
* Finds a field annotated {@code marker} within one test instance's class hierarchy.
*
* @throws IllegalStateException If more than one such field is found.
*/
private <A extends Annotation> FieldMatch findMarkedField(final Object instance,
final Class<A> marker)
{
final List<Field> fields =
AnnotationSupport.findAnnotatedFields(instance.getClass(), marker);
if (fields.size() > 1)
{
throw new IllegalStateException("Multiple @" + marker.getSimpleName()
+ " fields found in " + instance.getClass().getName() + ".");
}
if (fields.isEmpty())
{
return null;
}
final Field field = fields.get(0);
field.setAccessible(true);
return new FieldMatch(field, instance);
}
/**
* The original (3.5.0) field auto-scan, unchanged: within one instance's class hierarchy
* (most-derived class first), exactly one non-static field assignable to
* {@link IDatabaseTester} - two or more at the same declaring class is ambiguous. Now also
* tried across every test instance in scope, innermost first, for {@code @Nested} support.
*/
private IDatabaseTester autoScanTesterField(final List<Object> instances,
final Class<?> testClass) throws IllegalAccessException
{
for (final Object instance : instances)
{
Class<?> clazz = instance.getClass();
while (clazz != null && clazz != Object.class)
{
final Field field = findTesterField(clazz, instance);
if (field != null)
{
field.setAccessible(true);
final IDatabaseTester tester = (IDatabaseTester) field.get(instance);
if (tester == null)
{
throw new IllegalStateException("IDatabaseTester field '"
+ field.getName() + "' in " + instance.getClass().getName()
+ " is null.");
}
log.debug("Resolved IDatabaseTester '{}' in {}", field.getName(),
instance.getClass().getName());
return tester;
}
clazz = clazz.getSuperclass();
}
}
throw new IllegalStateException("No IDatabaseTester field found in "
+ testClass.getName() + " or its superclasses/enclosing classes. Declare a"
+ " non-static field whose type implements IDatabaseTester, mark it with"
+ " @DbUnitTester, or configure @DbUnitConfig(databaseTesterFactory = ...).");
}
private Field findTesterField(final Class<?> clazz, final Object instance)
{
Field match = null;
for (final Field field : clazz.getDeclaredFields())
{
if (!Modifier.isStatic(field.getModifiers())
&& IDatabaseTester.class.isAssignableFrom(field.getType()))
{
if (match != null)
{
throw new IllegalStateException("Multiple IDatabaseTester fields found in "
+ clazz.getName() + ": '" + match.getName() + "' and '"
+ field.getName() + "'. Declare exactly one non-static field"
+ " whose type implements IDatabaseTester in "
+ instance.getClass().getName() + ".");
}
match = field;
}
}
return match;
}
/**
* The resolved tester, and the test case when a {@code @DbUnitTestCase} field supplied one.
*/
static final class Resolution
{
final IDatabaseTester tester;
final PrepAndExpectedTestCase testCase;
private Resolution(final IDatabaseTester tester, final PrepAndExpectedTestCase testCase)
{
this.tester = tester;
this.testCase = testCase;
}
}
private static final class FieldMatch
{
private final Field field;
private final Object instance;
private FieldMatch(final Field field, final Object instance)
{
this.field = field;
this.instance = instance;
}
private Object value() throws IllegalAccessException
{
return field.get(instance);
}
}
private static final class MarkedFields
{
private final FieldMatch testCaseField;
private final FieldMatch testerField;
private MarkedFields(final FieldMatch testCaseField, final FieldMatch testerField)
{
this.testCaseField = testCaseField;
this.testerField = testerField;
}
}
}