SpringTesterResolver.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.spring;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.function.Predicate;
import org.dbunit.IDatabaseTester;
import org.dbunit.annotation.DbUnitTestCase;
import org.dbunit.annotation.DbUnitTester;
import org.dbunit.annotation.runtime.AnnotatedTestConfiguration;
import org.dbunit.annotation.runtime.TestInstanceTesterResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
* Resolves, for one test, the {@link IDatabaseTester} and (when present) the
* {@link org.dbunit.PrepAndExpectedTestCase} instance {@link DbUnitTestExecutionListener}
* drives, in the order that class's Javadoc documents: marked field, database tester factory,
* unmarked field, and last the {@code ApplicationContext} bean.
*
* <p>The field rules - which fields count, and what is ambiguous - are
* {@link TestInstanceTesterResolver}'s, shared with the JUnit 5 binding, so a mistake gets the
* same message under either. The test instances they search are those of a
* {@link TestInstanceChain}: the one Spring's {@link TestContext} holds and, for a
* {@code @Nested} class, the enclosing instances reachable from it, innermost first.
*
* <p>An enclosing instance that cannot be reached cannot be searched, so a tester field it
* declares is an error, never a silent fall-through to something that ranks lower, such as an
* {@code ApplicationContext} bean - unless a tester that outranks the field is found anyway: a
* marked field in a reachable instance outranks every field of an enclosing class, and the
* configured factory and an unmarked field in a reachable instance outrank an unmarked field of
* an enclosing class.
*
* @author Jeff Jensen
* @since 3.6.0
*/
class SpringTesterResolver
{
private static final Logger log = LoggerFactory.getLogger(SpringTesterResolver.class);
/**
* Resolves the tester and test case for the current test.
*
* @param testContext The test context.
* @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, no tester can be
* found, or a tester field of an enclosing class cannot be reached.
*/
TestInstanceTesterResolver.Resolution resolve(final TestContext testContext,
final AnnotatedTestConfiguration configuration) throws Exception
{
final Object testInstance = testContext.getTestInstance();
final Class<?> testClass = testContext.getTestClass();
final TestInstanceChain chain = TestInstanceChain.of(testInstance);
final List<Object> instances = chain.getInnermostFirst();
if (!anyDeclaresMarkedField(instances))
{
rejectUnreachableFields(chain, testClass, this::isMarked);
}
return TestInstanceTesterResolver.resolve(instances,
() -> findTester(instances, chain, testContext, configuration));
}
private IDatabaseTester findTester(final List<Object> instances, final TestInstanceChain chain,
final TestContext testContext, final AnnotatedTestConfiguration configuration)
throws Exception
{
final IDatabaseTester factoryTester =
TestInstanceTesterResolver.fromFactory(configuration);
if (factoryTester != null)
{
return factoryTester;
}
final IDatabaseTester fieldTester = TestInstanceTesterResolver.fromUnmarkedField(instances);
if (fieldTester != null)
{
return fieldTester;
}
final Class<?> testClass = testContext.getTestClass();
rejectUnreachableFields(chain, testClass, this::isUnmarkedTester);
final ApplicationContext applicationContext = findApplicationContext(testContext);
if (applicationContext != null)
{
final IDatabaseTester beanTester = findTesterBean(applicationContext);
if (beanTester != null)
{
return beanTester;
}
}
final boolean contextConsulted = applicationContext != null;
throw new IllegalStateException(noTesterMessage(testClass, contextConsulted));
}
/**
* Returns the test's {@code ApplicationContext}, or {@code null} when there is none to ask.
* A context Spring has not loaded yet - no listener has needed it so far, for example when
* the {@code DependencyInjectionTestExecutionListener} was dropped - is loaded here if the
* test class declares a context configuration to load it from.
*/
private ApplicationContext findApplicationContext(final TestContext testContext)
{
if (testContext.hasApplicationContext()
|| declaresContextConfiguration(testContext.getTestClass()))
{
return testContext.getApplicationContext();
}
return null;
}
private boolean declaresContextConfiguration(final Class<?> testClass)
{
return TestContextAnnotationUtils.hasAnnotation(testClass, ContextConfiguration.class)
|| TestContextAnnotationUtils.hasAnnotation(testClass, ContextHierarchy.class);
}
private IDatabaseTester findTesterBean(final ApplicationContext applicationContext)
{
final ObjectProvider<IDatabaseTester> beans =
applicationContext.getBeanProvider(IDatabaseTester.class);
final IDatabaseTester tester = beans.getIfUnique();
if (tester != null)
{
log.debug("Resolved IDatabaseTester bean of type '{}' from the ApplicationContext",
tester.getClass().getName());
}
return tester;
}
private String noTesterMessage(final Class<?> testClass, final boolean contextConsulted)
{
final String message = "No IDatabaseTester field or bean found for " + testClass.getName()
+ " or its superclasses/enclosing classes. Declare a non-static field whose type"
+ " implements IDatabaseTester, mark it with @DbUnitTester, expose a single"
+ " IDatabaseTester bean in the ApplicationContext, or configure"
+ " @DbUnitConfig(databaseTesterFactory = ...).";
if (contextConsulted)
{
return message;
}
return message + " The ApplicationContext was not loaded for this test, so no bean was"
+ " looked up: the test class declares no @ContextConfiguration or"
+ " @ContextHierarchy, or @TestExecutionListeners replaced Spring's default"
+ " listeners, dropping the DependencyInjectionTestExecutionListener that loads"
+ " the context - add mergeMode = MERGE_WITH_DEFAULTS.";
}
/**
* Returns whether any reachable test instance declares a field marked {@link DbUnitTester} or
* {@link DbUnitTestCase}: the field rules let the first such instance, innermost first, shadow
* every outer one entirely, so an enclosing instance that cannot be reached changes nothing.
*/
private boolean anyDeclaresMarkedField(final List<Object> instances)
{
for (final Object instance : instances)
{
final Field field = findField(instance.getClass(), this::isMarked);
if (field != null)
{
return true;
}
}
return false;
}
/**
* Fails when {@code matches} accepts a field of an enclosing class whose instance could not
* be reached, rather than carry on and silently drive a lower-ranked tester - e.g. an
* {@code ApplicationContext} bean - than the one that field would have supplied.
*/
private void rejectUnreachableFields(final TestInstanceChain chain, final Class<?> testClass,
final Predicate<Field> matches)
{
for (final Class<?> enclosingClass : chain.getUnreachableEnclosingClasses())
{
final Field field = findField(enclosingClass, matches);
if (field != null)
{
throw new IllegalStateException("The enclosing class " + enclosingClass.getName()
+ " declares the tester field '" + field.getName()
+ "', but the enclosing instance of the @Nested class "
+ testClass.getName() + " cannot be reached: when compiling for Java 18"
+ " or later, javac omits an inner class's reference to its enclosing"
+ " instance unless the class uses it. Use the enclosing instance"
+ " somewhere in the @Nested class, declare a @DbUnitTester field in the"
+ " @Nested class itself, or replace the enclosing class's tester field"
+ " with an IDatabaseTester bean in the ApplicationContext.");
}
}
}
/**
* Finds the first field anywhere in {@code clazz}'s hierarchy that {@code matches} accepts,
* most-derived class first.
*/
private Field findField(final Class<?> clazz, final Predicate<Field> matches)
{
Class<?> current = clazz;
while (current != null && current != Object.class)
{
for (final Field field : current.getDeclaredFields())
{
if (matches.test(field))
{
return field;
}
}
current = current.getSuperclass();
}
return null;
}
/** Whether the field is marked {@link DbUnitTester} or {@link DbUnitTestCase}. */
private boolean isMarked(final Field field)
{
return field.isAnnotationPresent(DbUnitTester.class)
|| field.isAnnotationPresent(DbUnitTestCase.class);
}
/** Whether the field is one the 3.5.0 auto-scan uses: non-static and an IDatabaseTester. */
private boolean isUnmarkedTester(final Field field)
{
return !Modifier.isStatic(field.getModifiers())
&& IDatabaseTester.class.isAssignableFrom(field.getType());
}
}