DbUnitExtension.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.Executable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.dbunit.IDatabaseTester;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.annotation.DbUnitConfig;
import org.dbunit.annotation.DbUnitExpected;
import org.dbunit.annotation.DbUnitPrep;
import org.dbunit.annotation.DbUnitRowCountCheck;
import org.dbunit.annotation.DbUnitSetup;
import org.dbunit.annotation.DbUnitTearDown;
import org.dbunit.annotation.DbUnitTestCase;
import org.dbunit.annotation.DbUnitTester;
import org.dbunit.annotation.runtime.AnnotatedTestConfiguration;
import org.dbunit.annotation.runtime.AnnotatedTestExecutor;
import org.dbunit.database.IDatabaseConnection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.AnnotationSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * JUnit 5/6 extension for DbUnit that manages the dbUnit lifecycle around each test method,
 * driven by the {@code org.dbunit.annotation} family: {@link DbUnitPrep}, {@link DbUnitSetup},
 * {@link DbUnitExpected}, {@link DbUnitTearDown}, and {@link DbUnitConfig}. Add
 * {@link DbUnitTest @DbUnitTest} (or {@code @ExtendWith(DbUnitExtension.class)} directly) to
 * opt a test class in.
 *
 * <h2>The two paths</h2>
 *
 * <p>Without {@link DbUnitExpected}, this is the setup/teardown path: the
 * {@link DbUnitPrep} dataset (if any) and {@link DbUnitSetup} operation are applied, then
 * {@link IDatabaseTester#onSetup()} runs before the test method and
 * {@link IDatabaseTester#onTearDown()} after it. The test method asserts results however it
 * likes.
 *
 * <p>With {@link DbUnitExpected} present, the test switches onto the
 * {@link PrepAndExpectedTestCase} prep/expected path: {@code configureTest()} and
 * {@code preTest()} run before the test method, {@code postTest()} after it - {@code postTest()}
 * compares the database against the {@link DbUnitExpected} dataset, so the verifying is done
 * for you, skipping it when the test method itself already failed so a verification failure
 * never masks the real cause. Neither path is a reduced form of the other; they run different
 * lifecycles.
 *
 * <pre>{@code
 * @DbUnitTest
 * class AccountRepositoryTest {
 *     IDatabaseTester databaseTester;
 *
 *     AccountRepositoryTest() throws ClassNotFoundException {
 *         databaseTester = new JdbcDatabaseTester("driver", "url", "user", "pass");
 *     }
 *
 *     @Test
 *     @DbUnitPrep("accounts-prep.xml")
 *     @DbUnitExpected(value = "accounts-expected.xml", verifyTables = {"ACCOUNT"})
 *     void testWithdraw_sufficientBalance_decrementsBalance() { ... }
 * }
 * }</pre>
 *
 * <h2>Resolving the tester or test case</h2>
 *
 * <p>First match wins:
 * <ol>
 *   <li>A field annotated {@link DbUnitTestCase}, whose type implements
 *   {@link PrepAndExpectedTestCase} - that instance is driven directly.</li>
 *   <li>A field annotated {@link DbUnitTester}, whose type implements
 *   {@link IDatabaseTester}.</li>
 *   <li>{@link DbUnitConfig#databaseTesterFactory()}, reflectively instantiated and asked to
 *   create a tester.</li>
 *   <li>The original (3.5.0) auto-scan: exactly one non-static field assignable to
 *   {@link IDatabaseTester}, nearest declaring class wins - unchanged, so every 3.5.0 test
 *   keeps working untouched.</li>
 * </ol>
 *
 * <p>Field discovery walks every test instance in scope, innermost first, so a {@code @Nested}
 * test class inherits its enclosing class's tester field.
 *
 * <h2>Programmatic setup</h2>
 *
 * <p>Without annotations, configure the tester - including its dataset - in a
 * {@code @BeforeEach} method; those run before this extension's setup callback:
 *
 * <pre>{@code
 * @ExtendWith(DbUnitExtension.class)
 * class MyDatabaseTest {
 *     IDatabaseTester databaseTester;
 *
 *     MyDatabaseTest() throws ClassNotFoundException {
 *         databaseTester = new JdbcDatabaseTester("driver", "url", "user", "pass");
 *     }
 *
 *     @BeforeEach
 *     void loadDataset() throws Exception {
 *         databaseTester.setDataSet(new FlatXmlDataSetBuilder().build(...));
 *     }
 *
 *     @Test
 *     void testSomething() { ... }
 * }
 * }</pre>
 *
 * <p>This zero-annotation style is the 3.5.0 lifecycle: the extension calls only
 * {@code onSetup()}/{@code onTearDown()} on the tester and runs the row count check around
 * them, without replacing the tester's {@link org.dbunit.IOperationListener}. The connection it
 * resolves for the row count baseline is never closed before {@code onSetup()} - a tester that
 * returns one fixed connection from every call (e.g. a {@code DefaultDatabaseTester} built from
 * a fixed connection) would otherwise run {@code onSetup()} against a closed one - and is
 * closed after the test when {@code closeConnectionAfterTest} allows and the tester's listener
 * is not {@code NO_OP}. Adding any {@code @DbUnit*} annotation or a
 * {@link DbUnitTester @DbUnitTester}/{@link DbUnitTestCase @DbUnitTestCase} field opts the test
 * into the fuller annotation-driven lifecycle described above.
 *
 * <h2>Parameter injection</h2>
 *
 * <p>Also a {@link ParameterResolver} for {@link IDatabaseTester}, {@link PrepAndExpectedTestCase},
 * {@link IDatabaseConnection}, and {@link Connection} parameters on {@code @Test} and
 * {@code @BeforeEach} methods - but only for a test that opts into the
 * {@code org.dbunit.annotation} family: one carrying {@link DbUnitTest @DbUnitTest} or any
 * {@code @DbUnit*} annotation ({@link DbUnitConfig}, {@link DbUnitPrep}, {@link DbUnitSetup},
 * {@link DbUnitExpected}, {@link DbUnitTearDown}, {@link DbUnitRowCountCheck}) on the class or
 * method, or a {@link DbUnitTester @DbUnitTester}/{@link DbUnitTestCase @DbUnitTestCase} field.
 * A bare {@code @ExtendWith(DbUnitExtension.class)} class with only a plain, unannotated
 * {@link IDatabaseTester} field keeps the 3.5.0 lifecycle behaviour untouched and has no
 * parameter claimed here, so another extension resolving a {@link Connection} (or any of the
 * other three types) on the same test stays unambiguous. Switch such a class to
 * {@link DbUnitTest @DbUnitTest}, or mark its field {@link DbUnitTester @DbUnitTester}, to opt
 * its parameters in.
 *
 * <p><strong>On an annotation-driven test this extension claims <em>every</em>
 * {@link Connection} parameter</strong> on a {@code @Test}/{@code @BeforeEach} method. Another
 * extension on the same test that also resolves {@code java.sql.Connection} - Spring's
 * {@code SpringExtension}, Testcontainers, a JPA harness - then collides with JUnit's
 * "discovered multiple competing ParameterResolvers". Set
 * {@link DbUnitConfig#injectConnectionParameter() @DbUnitConfig(injectConnectionParameter = false)}
 * to yield the bare {@code Connection} to that extension; the dbUnit-specific
 * {@link IDatabaseConnection} parameter is still resolved (no other framework claims that
 * type) - inject it and call {@code getConnection()}.
 *
 * <p>The injected {@link IDatabaseConnection}/{@link Connection} is managed for the test's
 * duration and closed afterward - by this extension on the setup/teardown path, by the
 * driven {@link PrepAndExpectedTestCase} on the prep/expected path; do not close it
 * yourself. A {@code @BeforeEach} parameter resolves
 * before {@link #beforeTestExecution(ExtensionContext)} runs - JUnit Jupiter always calls
 * {@code @BeforeEach} methods first - so it sees the tester/connection exactly as they exist
 * before this extension's own setup: {@link DbUnitPrep}'s dataset, if any, is not loaded onto
 * it yet.
 *
 * <p>Not safe for concurrent execution of test methods that resolve to the same tester or test
 * case instance - e.g. a {@code static} {@link DbUnitTester @DbUnitTester}/
 * {@link DbUnitTestCase @DbUnitTestCase} field, or any instance field at all under
 * {@code @TestInstance(Lifecycle.PER_CLASS)} - since this extension applies annotations and
 * drives the dbUnit lifecycle against it without synchronization. Run such test methods
 * sequentially (JUnit Jupiter's own default) rather than under
 * {@code junit.jupiter.execution.parallel.enabled=true}, or give each test method's own tester
 * its own non-shared field.
 *
 * @author Jeff Jensen
 * @since 3.5.0
 * @see DbUnitPrep
 * @see DbUnitSetup
 * @see DbUnitExpected
 * @see DbUnitTearDown
 * @see DbUnitConfig
 * @see DbUnitRowCountCheck
 * @see DbUnitTest
 */
public class DbUnitExtension
        implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver
{
    private static final Logger log = LoggerFactory.getLogger(DbUnitExtension.class);

    private static final ExtensionContext.Namespace NAMESPACE =
            ExtensionContext.Namespace.create(DbUnitExtension.class);

    /** Package-visible so tests can reference it instead of duplicating the literal. */
    static final String EXECUTOR_KEY = "annotatedTestExecutor";

    private final TesterResolver testerResolver = new TesterResolver();

    /** Creates the extension. */
    public DbUnitExtension()
    {
    }

    /**
     * Resolves the configured {@link AnnotatedTestExecutor} - reusing one already resolved for
     * this test by parameter injection into a {@code @BeforeEach} method, if any - and runs its
     * before-test steps.
     *
     * @param context The extension context for the test method.
     * @throws Exception If resolving the tester or test case, or running the before-test
     *             steps, fails.
     */
    @Override
    public void beforeTestExecution(final ExtensionContext context) throws Exception
    {
        resolveExecutor(context).beforeTest();
    }

    /**
     * Runs the stored {@link AnnotatedTestExecutor}'s after-test steps.
     *
     * <p>A failure here is simply left to propagate rather than caught and reconciled by hand:
     * the JUnit Platform engine invokes every {@code AfterTestExecutionCallback} through the
     * same {@code ThrowableCollector} that already collected the test method's own outcome (if
     * any), so the engine itself already applies the correct precedence - a genuine failure
     * here is promoted over a mere test <em>abort</em> from the test method (e.g. a failed
     * {@code Assumptions.assumeTrue()}), with the abort attached as a suppressed exception on
     * the promoted failure instead of the failure being silently swallowed into the discarded
     * abort; against a real test method failure, this failure is the one attached as
     * suppressed, so JUnit still reports the original failure as the cause. Reimplementing
     * that precedence here would only risk getting it wrong.
     *
     * @param context The extension context for the test method.
     * @throws Exception If the after-test steps fail.
     */
    @Override
    public void afterTestExecution(final ExtensionContext context) throws Exception
    {
        final AnnotatedTestExecutor executor =
                context.getStore(NAMESPACE).get(EXECUTOR_KEY, AnnotatedTestExecutor.class);
        if (executor == null)
        {
            return;
        }

        final boolean testFailed = context.getExecutionException().isPresent();
        executor.afterTest(testFailed);
    }

    /**
     * Reports whether this extension resolves the given parameter: an {@link IDatabaseTester},
     * {@link PrepAndExpectedTestCase}, {@link IDatabaseConnection}, or {@link Connection}
     * parameter, declared on the current test method or a {@code @BeforeEach} method - the only
     * two contexts {@link #afterTestExecution(ExtensionContext)} still runs after, so a
     * connection this executor resolves is guaranteed to still be open, and eventually closed,
     * for the parameter's entire lifetime. A matching parameter on any other method
     * (constructor, {@code @BeforeAll}, {@code @AfterEach}, {@code @AfterAll}) is declined here
     * rather than handed out already-closed - or, worse, opened for the first time with nothing
     * left to ever close it - since {@code @AfterEach} and {@code @AfterAll} run after
     * {@link #afterTestExecution(ExtensionContext)} already has.
     *
     * <p>Also declined - regardless of type or method - unless the test opts into the
     * {@code org.dbunit.annotation} family (see {@link #isAnnotationDriven(ExtensionContext)}).
     * A bare {@code @ExtendWith(DbUnitExtension.class)} class with only a plain, unannotated
     * {@link IDatabaseTester} field is the 3.5.0 lifecycle-only style: it never claimed a
     * parameter before this method existed, and does not now, so another extension resolving
     * one of these types - {@link Connection} especially - on the same test is not left
     * competing with an unconditional claim here. An annotation-driven test can still opt its
     * bare {@link Connection} parameter back out with
     * {@link DbUnitConfig#injectConnectionParameter() @DbUnitConfig(injectConnectionParameter = false)}
     * while keeping the other three types.
     *
     * @param parameterContext The parameter to resolve.
     * @param extensionContext The extension context for the test method.
     * @return {@code true} for a matching parameter type on the test method or a
     *         {@code @BeforeEach} method of an annotation-driven test.
     * @throws ParameterResolutionException Never thrown directly; declared by the
     *             {@link ParameterResolver} contract.
     */
    @Override
    public boolean supportsParameter(final ParameterContext parameterContext,
            final ExtensionContext extensionContext) throws ParameterResolutionException
    {
        final Class<?> type = parameterContext.getParameter().getType();
        final InjectableParameter injectable = InjectableParameter.forType(type);
        final boolean typeSupported = injectable != null
                && (injectable != InjectableParameter.JDBC_CONNECTION
                        || claimsBareConnectionParameter(extensionContext));
        return typeSupported
                && isTestMethodOrBeforeEach(parameterContext, extensionContext)
                && isAnnotationDriven(extensionContext);
    }

    /**
     * Returns whether this extension resolves a bare {@link Connection} parameter for the
     * current test - {@link DbUnitConfig#injectConnectionParameter()}, {@code true} by default.
     * Set {@code false} to yield {@code java.sql.Connection} to a co-registered resolver
     * (Spring, Testcontainers, ...); the {@link IDatabaseConnection} parameter is unaffected.
     */
    private boolean claimsBareConnectionParameter(final ExtensionContext context)
    {
        final DbUnitConfig config = findAnnotation(context, DbUnitConfig.class);
        return config == null || config.injectConnectionParameter();
    }

    /** The class/method annotations any one of which opts a test into parameter injection. */
    private static final List<Class<? extends Annotation>> OPT_IN_ANNOTATIONS = Arrays.asList(
            DbUnitTest.class, DbUnitConfig.class, DbUnitPrep.class, DbUnitSetup.class,
            DbUnitExpected.class, DbUnitTearDown.class, DbUnitRowCountCheck.class);

    /**
     * Returns whether the current test opts into the {@code org.dbunit.annotation} family, and
     * so into parameter injection: any {@link #OPT_IN_ANNOTATIONS} annotation on the test method
     * or the class hierarchy (enclosing classes included, for {@code @Nested}), or a
     * {@link DbUnitTester}/{@link DbUnitTestCase} field anywhere in that same scope. A plain,
     * unannotated {@link IDatabaseTester} field alone is not an opt-in - that is the 3.5.0
     * lifecycle-only style, left exactly as it was.
     */
    private boolean isAnnotationDriven(final ExtensionContext context)
    {
        for (final Class<? extends Annotation> annotationType : OPT_IN_ANNOTATIONS)
        {
            if (findAnnotation(context, annotationType) != null)
            {
                return true;
            }
        }
        return hasMarkerField(context.getRequiredTestClass())
                || context.getEnclosingTestClasses().stream().anyMatch(this::hasMarkerField);
    }

    /**
     * Returns whether {@code testClass} or a superclass declares a {@link DbUnitTester} or
     * {@link DbUnitTestCase} field.
     */
    private boolean hasMarkerField(final Class<?> testClass)
    {
        return !AnnotationSupport.findAnnotatedFields(testClass, DbUnitTester.class).isEmpty()
                || !AnnotationSupport.findAnnotatedFields(testClass, DbUnitTestCase.class)
                        .isEmpty();
    }

    /**
     * Returns whether {@code parameterContext}'s declaring method is the current test method or
     * a {@code @BeforeEach} method - see {@link #supportsParameter(ParameterContext, ExtensionContext)}.
     */
    private boolean isTestMethodOrBeforeEach(final ParameterContext parameterContext,
            final ExtensionContext extensionContext)
    {
        final Executable declaringExecutable = parameterContext.getDeclaringExecutable();
        if (!(declaringExecutable instanceof Method))
        {
            return false;
        }
        final Method method = (Method) declaringExecutable;
        return extensionContext.getTestMethod().map(method::equals).orElse(false)
                || AnnotationSupport.isAnnotated(method, BeforeEach.class);
    }

    /**
     * Resolves a parameter from the same {@link AnnotatedTestExecutor} that
     * {@link #beforeTestExecution(ExtensionContext)} drives - reusing it (creating and caching
     * it on first use, for a {@code @BeforeEach} parameter resolved before
     * {@code beforeTestExecution} runs) rather than resolving a second tester or test case
     * independently.
     *
     * <p>A {@link PrepAndExpectedTestCase} parameter requires a {@link DbUnitTestCase @DbUnitTestCase}
     * field: without one, the executor may not have constructed its own instance yet - see
     * {@link AnnotatedTestExecutor#getPrepAndExpectedTestCase()} - so there is no single
     * instance to inject.
     *
     * @param parameterContext The parameter to resolve.
     * @param extensionContext The extension context for the test method.
     * @return The resolved {@link IDatabaseTester}, {@link PrepAndExpectedTestCase},
     *         {@link IDatabaseConnection}, or {@link Connection} value.
     * @throws ParameterResolutionException If no test instance exists yet, or if resolution
     *             otherwise fails. The first is not reachable through JUnit Jupiter's own
     *             engine for a parameter this class actually claims - a static-scope context
     *             such as {@code @BeforeAll} is already declined by
     *             {@link #supportsParameter(ParameterContext, ExtensionContext)}, so the
     *             engine never calls this method for it at all - only a caller invoking this
     *             method directly, without checking that first, could still hit it.
     */
    @Override
    public Object resolveParameter(final ParameterContext parameterContext,
            final ExtensionContext extensionContext) throws ParameterResolutionException
    {
        final Class<?> type = parameterContext.getParameter().getType();
        if (!extensionContext.getTestInstances().isPresent())
        {
            throw new ParameterResolutionException("Cannot inject a dbUnit parameter of type "
                    + type.getName() + " here; IDatabaseTester, PrepAndExpectedTestCase,"
                    + " IDatabaseConnection, and Connection parameters are only supported for"
                    + " @Test and @BeforeEach methods, where a test instance already exists -"
                    + " not @BeforeAll or other static-scope contexts.");
        }
        try
        {
            final AnnotatedTestExecutor executor = resolveExecutor(extensionContext);
            final InjectableParameter injectable = InjectableParameter.forType(type);
            if (injectable == null)
            {
                throw new IllegalStateException("resolveParameter() reached for unsupported"
                        + " type " + type.getName() + "; supportsParameter() should have"
                        + " declined it.");
            }
            return injectable.resolve(executor);
        } catch (final Exception e)
        {
            throw new ParameterResolutionException("Failed to resolve a dbUnit parameter of"
                    + " type " + type.getName() + ".", e);
        }
    }

    /**
     * Resolves and caches the {@link AnnotatedTestExecutor} for the current test, so
     * {@link #beforeTestExecution(ExtensionContext)} and
     * {@link #resolveParameter(ParameterContext, ExtensionContext)} - whichever runs first,
     * since a {@code @BeforeEach} parameter resolves before {@code beforeTestExecution} does -
     * both drive the same tester and test case instead of each resolving their own.
     */
    private AnnotatedTestExecutor resolveExecutor(final ExtensionContext context)
            throws Exception
    {
        final ExtensionContext.Store store = context.getStore(NAMESPACE);
        AnnotatedTestExecutor executor = store.get(EXECUTOR_KEY, AnnotatedTestExecutor.class);
        if (executor == null)
        {
            final AnnotatedTestConfiguration configuration = resolveConfiguration(context);
            final TesterResolver.Resolution resolution =
                    testerResolver.resolve(context, configuration);
            executor = new AnnotatedTestExecutor(configuration, resolution.tester,
                    resolution.testCase, isAnnotationDriven(context));
            store.put(EXECUTOR_KEY, executor);
        }
        return executor;
    }

    // ---- annotation resolution ----

    private AnnotatedTestConfiguration resolveConfiguration(final ExtensionContext context)
    {
        final Class<?> testClass = context.getRequiredTestClass();
        final DbUnitConfig config = findAnnotation(context, DbUnitConfig.class);
        final DbUnitPrep prep = findAnnotation(context, DbUnitPrep.class);
        final DbUnitSetup setup = findAnnotation(context, DbUnitSetup.class);
        final DbUnitExpected expected = findAnnotation(context, DbUnitExpected.class);
        final DbUnitTearDown tearDown = findAnnotation(context, DbUnitTearDown.class);
        final DbUnitRowCountCheck rowCountCheck =
                findAnnotation(context, DbUnitRowCountCheck.class);
        return AnnotatedTestConfiguration.from(testClass, config, prep, setup, expected,
                tearDown, rowCountCheck);
    }

    /**
     * Finds {@code annotationType}, trying the test method first and the class hierarchy -
     * including enclosing classes, for {@code @Nested} support - second, so a method-level
     * annotation continues to win over a class-level one.
     */
    private <A extends Annotation> A findAnnotation(final ExtensionContext context,
            final Class<A> annotationType)
    {
        final Optional<Method> method = context.getTestMethod();
        if (method.isPresent())
        {
            final Optional<A> onMethod =
                    AnnotationSupport.findAnnotation(method.get(), annotationType);
            if (onMethod.isPresent())
            {
                return onMethod.get();
            }
        }
        return AnnotationSupport
                .findAnnotation(context.getRequiredTestClass(), annotationType,
                        context.getEnclosingTestClasses())
                .orElse(null);
    }

    /**
     * The parameter types this extension resolves, each paired with how to obtain its value
     * from the resolved {@link AnnotatedTestExecutor}, in resolution order.
     * {@link #supportsParameter(ParameterContext, ExtensionContext)} and
     * {@link #resolveParameter(ParameterContext, ExtensionContext)} both consult this one list,
     * so a supported type cannot be declared in one and forgotten in the other. The bare
     * {@link Connection} opt-out ({@link #claimsBareConnectionParameter(ExtensionContext)}) is a
     * separate gate layered on top, since it turns one entry off per test rather than adding or
     * removing a type.
     */
    private enum InjectableParameter
    {
        /** A {@link PrepAndExpectedTestCase}, from the {@code @DbUnitTestCase} field. */
        PREP_AND_EXPECTED_TEST_CASE(PrepAndExpectedTestCase.class)
        {
            @Override
            Object resolve(final AnnotatedTestExecutor executor)
            {
                final PrepAndExpectedTestCase testCase = executor.getPrepAndExpectedTestCase();
                if (testCase == null)
                {
                    throw new IllegalStateException("No PrepAndExpectedTestCase available to"
                            + " inject; declare a @DbUnitTestCase field so there is a single"
                            + " instance to use.");
                }
                return testCase;
            }
        },

        /** The {@link IDatabaseTester} this executor drives. */
        DATABASE_TESTER(IDatabaseTester.class)
        {
            @Override
            Object resolve(final AnnotatedTestExecutor executor)
            {
                return executor.getTester();
            }
        },

        /** The {@link IDatabaseConnection} this test's steps share. */
        DATABASE_CONNECTION(IDatabaseConnection.class)
        {
            @Override
            Object resolve(final AnnotatedTestExecutor executor) throws Exception
            {
                return requireConnection(executor);
            }
        },

        /** The underlying JDBC {@link Connection}, unwrapped from the shared {@link IDatabaseConnection}. */
        JDBC_CONNECTION(Connection.class)
        {
            @Override
            Object resolve(final AnnotatedTestExecutor executor) throws Exception
            {
                return requireConnection(executor).getConnection();
            }
        };

        private final Class<?> parameterType;

        InjectableParameter(final Class<?> parameterType)
        {
            this.parameterType = parameterType;
        }

        /**
         * Obtains this parameter's value from the resolved executor.
         *
         * @param executor The executor driving the current test.
         * @return The value to inject.
         * @throws Exception If resolving the value fails.
         */
        abstract Object resolve(AnnotatedTestExecutor executor) throws Exception;

        private static IDatabaseConnection requireConnection(final AnnotatedTestExecutor executor)
                throws Exception
        {
            final IDatabaseConnection connection = executor.getConnection();
            if (connection == null)
            {
                throw new IllegalStateException("IDatabaseTester#getConnection() returned"
                        + " null; cannot inject an IDatabaseConnection or Connection"
                        + " parameter.");
            }
            return connection;
        }

        /**
         * Returns the entry whose type {@code parameterType} is assignable to, or {@code null}
         * when none matches. The first match wins, so an entry earlier in declaration order
         * shadows a later one for a type assignable to both.
         *
         * @param parameterType The declared parameter type.
         * @return The matching entry, or {@code null}.
         */
        static InjectableParameter forType(final Class<?> parameterType)
        {
            for (final InjectableParameter candidate : values())
            {
                if (candidate.parameterType.isAssignableFrom(parameterType))
                {
                    return candidate;
                }
            }
            return null;
        }
    }
}