TestInstanceChain.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.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
* A Spring test instance and, for a JUnit Jupiter {@code @Nested} test class, the enclosing
* instances Spring's {@code TestContext} does not hand over: the instance of the class enclosing
* the {@code @Nested} class, the instance enclosing that, and so on outward.
*
* <p>Spring's {@code TestContext} holds only the innermost instance, but a {@code @Nested}
* class's tester field may be declared on an enclosing class, as it may be under
* {@code DbUnitExtension}. An enclosing instance is read from the compiler-generated reference
* every inner class holds to it. When compiling for Java 18 or later, {@code javac} omits that
* reference from an inner class that never uses its enclosing instance, so the chain then stops
* there, and {@link #getUnreachableEnclosingClasses()} names the classes it could not reach. The
* chain also stops, by design, where Spring does not inherit an enclosing class's configuration:
* for a {@code static} nested class, or one annotated
* {@code @NestedTestConfiguration(OVERRIDE)} - see
* {@link TestContextAnnotationUtils#searchEnclosingClass(Class)}.
*
* @author Jeff Jensen
* @since 3.6.0
*/
class TestInstanceChain
{
private final List<Object> innermostFirst;
private final List<Class<?>> unreachableEnclosingClasses;
private TestInstanceChain(final List<Object> innermostFirst,
final List<Class<?>> unreachableEnclosingClasses)
{
this.innermostFirst = Collections.unmodifiableList(innermostFirst);
this.unreachableEnclosingClasses =
Collections.unmodifiableList(unreachableEnclosingClasses);
}
/**
* Builds the chain that starts at {@code testInstance}.
*
* @param testInstance The innermost test instance, the one Spring's {@code TestContext}
* holds.
* @return The chain.
*/
static TestInstanceChain of(final Object testInstance)
{
final List<Object> instances = new ArrayList<>();
Object current = testInstance;
while (true)
{
instances.add(current);
final Class<?> currentClass = current.getClass();
if (!TestContextAnnotationUtils.searchEnclosingClass(currentClass))
{
return new TestInstanceChain(instances, Collections.emptyList());
}
final Object enclosing = readEnclosingInstance(current);
if (enclosing == null)
{
final List<Class<?>> unreachable = enclosingClassesOf(currentClass);
return new TestInstanceChain(instances, unreachable);
}
current = enclosing;
}
}
/**
* Returns the test class and, for a {@code @Nested} class, each class enclosing it whose
* configuration Spring inherits, innermost first. Unlike a chain, it needs no instance, so
* it is the same whether or not the enclosing instances can be reached.
*
* @param testClass The innermost test class.
* @return The classes in scope.
*/
static List<Class<?>> classesInScope(final Class<?> testClass)
{
final List<Class<?>> classes = new ArrayList<>();
classes.add(testClass);
classes.addAll(enclosingClassesOf(testClass));
return classes;
}
/**
* Returns the test instances in scope.
*
* @return The innermost instance first, then each reachable enclosing instance outward.
*/
List<Object> getInnermostFirst()
{
return innermostFirst;
}
/**
* Returns the enclosing classes whose instances could not be reached although Spring
* inherits their configuration.
*
* @return The classes outward from the last reachable instance; empty when the chain is
* complete.
*/
List<Class<?>> getUnreachableEnclosingClasses()
{
return unreachableEnclosingClasses;
}
private static List<Class<?>> enclosingClassesOf(final Class<?> innerClass)
{
final List<Class<?>> enclosingClasses = new ArrayList<>();
Class<?> current = innerClass;
while (TestContextAnnotationUtils.searchEnclosingClass(current))
{
current = current.getEnclosingClass();
enclosingClasses.add(current);
}
return enclosingClasses;
}
/**
* Reads the enclosing instance from the synthetic instance field of type
* {@link Class#getEnclosingClass()} that {@code javac} generates in an inner class.
*
* @return The enclosing instance, or {@code null} when the class holds no such reference.
*/
private static Object readEnclosingInstance(final Object innerInstance)
{
final Class<?> innerClass = innerInstance.getClass();
final Class<?> enclosingClass = innerClass.getEnclosingClass();
for (final Field field : innerClass.getDeclaredFields())
{
if (field.isSynthetic() && !Modifier.isStatic(field.getModifiers())
&& field.getType() == enclosingClass)
{
field.setAccessible(true);
try
{
return field.get(innerInstance);
} catch (final IllegalAccessException e)
{
throw new IllegalStateException("Cannot read the enclosing instance of "
+ innerClass.getName() + " from field '" + field.getName() + "'.", e);
}
}
}
return null;
}
}