diff --git a/src/main/java/org/dbunit/junit/jupiter/DbUnitExtension.java b/src/main/java/org/dbunit/junit/jupiter/DbUnitExtension.java
new file mode 100644
index 000000000..d8190302e
--- /dev/null
+++ b/src/main/java/org/dbunit/junit/jupiter/DbUnitExtension.java
@@ -0,0 +1,154 @@
+/*
+ *
+ * The DbUnit Database Testing Framework
+ * Copyright (C)2002-2025, 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.reflect.Field;
+import java.lang.reflect.Modifier;
+
+import org.dbunit.IDatabaseTester;
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JUnit 5/6 extension for DbUnit that manages the {@link IDatabaseTester} lifecycle
+ * around each test method.
+ *
+ * Calls {@link IDatabaseTester#onSetup()} immediately before the test method
+ * (after all {@code @BeforeEach} callbacks) and {@link IDatabaseTester#onTearDown()}
+ * immediately after the test method (before any {@code @AfterEach} callbacks).
+ *
+ *
The extension discovers the {@link IDatabaseTester} by scanning the test
+ * instance's fields, including inherited fields, for a non-static field assignable
+ * to {@link IDatabaseTester}: the nearest declaring class (test class before
+ * superclass) wins, but that class must declare exactly one such field—two or
+ * more at the same level is rejected as ambiguous. Configure the tester—including
+ * its dataset—in a {@code @BeforeEach} method; those run before this extension's
+ * setup callback:
+ *
+ *
{@code
+ * @ExtendWith(DbUnitExtension.class)
+ * class MyDatabaseTest {
+ * IDatabaseTester databaseTester = new JdbcDatabaseTester("driver", "url", "user", "pass");
+ *
+ * @BeforeEach
+ * void loadDataset() throws Exception {
+ * databaseTester.setDataSet(new FlatXmlDataSetBuilder().build(...));
+ * }
+ *
+ * @Test
+ * void testSomething() { ... }
+ * }
+ * }
+ *
+ * Note: {@code @Nested} test classes are not supported. Field
+ * discovery only scans the innermost test instance and its superclasses—not
+ * enclosing class instances—since a Java nested class does not extend its
+ * enclosing class.
+ *
+ * @author dbunit
+ * @since 3.4.1
+ */
+public class DbUnitExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
+ private static final Logger log = LoggerFactory.getLogger(DbUnitExtension.class);
+
+ private static final ExtensionContext.Namespace NAMESPACE =
+ ExtensionContext.Namespace.create(DbUnitExtension.class);
+
+ private static final String TESTER_KEY = "databaseTester";
+
+ /**
+ * Runs database setup before the test method executes.
+ *
+ * @param context The extension context for the test method.
+ * @throws Exception If resolving the {@link IDatabaseTester} field or its onSetup() call fails.
+ */
+ @Override
+ public void beforeTestExecution(final ExtensionContext context) throws Exception {
+ final IDatabaseTester tester = resolveTester(context);
+ context.getStore(NAMESPACE).put(TESTER_KEY, tester);
+ tester.onSetup();
+ }
+
+ /**
+ * Runs database teardown after the test method executes.
+ *
+ * @param context The extension context for the test method.
+ * @throws Exception If the stored {@link IDatabaseTester}'s onTearDown() call fails.
+ */
+ @Override
+ public void afterTestExecution(final ExtensionContext context) throws Exception {
+ final IDatabaseTester tester =
+ context.getStore(NAMESPACE).get(TESTER_KEY, IDatabaseTester.class);
+ if (tester != null) {
+ tester.onTearDown();
+ }
+ }
+
+ private IDatabaseTester resolveTester(final ExtensionContext context) throws Exception {
+ final Object testInstance = context.getTestInstance()
+ .orElseThrow(() -> new IllegalStateException(
+ "No test instance available in ExtensionContext."));
+
+ Class> clazz = testInstance.getClass();
+ while (clazz != null && clazz != Object.class) {
+ final Field field = findTesterField(clazz, testInstance);
+ if (field != null) {
+ field.setAccessible(true);
+ final IDatabaseTester tester = (IDatabaseTester) field.get(testInstance);
+ if (tester == null) {
+ throw new IllegalStateException("IDatabaseTester field '"
+ + field.getName() + "' in "
+ + testInstance.getClass().getName() + " is null.");
+ }
+ log.debug("Resolved IDatabaseTester '{}' in {}",
+ field.getName(), testInstance.getClass().getName());
+ return tester;
+ }
+ clazz = clazz.getSuperclass();
+ }
+
+ throw new IllegalStateException("No IDatabaseTester field found in "
+ + testInstance.getClass().getName()
+ + " or its superclasses. Declare a non-static field whose type implements IDatabaseTester"
+ + " to use DbUnitExtension.");
+ }
+
+ private Field findTesterField(final Class> clazz, final Object testInstance) {
+ 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 "
+ + testInstance.getClass().getName() + ".");
+ }
+ match = field;
+ }
+ }
+ return match;
+ }
+}
diff --git a/src/site/asciidoc/testcases.adoc b/src/site/asciidoc/testcases.adoc
index 5717c5863..dc89e15f8 100644
--- a/src/site/asciidoc/testcases.adoc
+++ b/src/site/asciidoc/testcases.adoc
@@ -22,6 +22,10 @@ that predate this recommendation; see
link:testcases/MigratingToIDatabaseTester.html[Migrating to IDatabaseTester]
if you want to convert an existing `DBTestCase` subclass.
+On JUnit 5/6 (Jupiter), composition users can also skip writing the
+`@BeforeEach`/`@AfterEach` lifecycle calls entirely — see
+link:testcases/DbUnitExtension.html[DbUnitExtension].
+
Whichever style you choose,
link:testcases/PrepAndExpectedTestCase.html[PrepAndExpectedTestCase] is
usually a better starting point than driving either one directly — it
diff --git a/src/site/asciidoc/testcases/DbUnitExtension.adoc b/src/site/asciidoc/testcases/DbUnitExtension.adoc
new file mode 100644
index 000000000..28ce4ebc2
--- /dev/null
+++ b/src/site/asciidoc/testcases/DbUnitExtension.adoc
@@ -0,0 +1,67 @@
+= DbUnitExtension
+Jeff Jensen
+
+== Overview
+
+link:/dbunit/apidocs/org/dbunit/junit/jupiter/DbUnitExtension.html[DbUnitExtension]
+is a JUnit 5/6 (Jupiter) extension that drives the
+link:IDatabaseTester.html[IDatabaseTester] setup/teardown lifecycle
+automatically, so a test class doesn't need its own `@BeforeEach`/`@AfterEach`
+pair calling `onSetup()`/`onTearDown()`.
+
+Register it with `@ExtendWith(DbUnitExtension.class)`. The test class still
+holds its own `IDatabaseTester` field — this is the same composition style as
+link:IDatabaseTester.html[the IDatabaseTester guide], just with the lifecycle
+calls automated instead of written by hand:
+
+[source,java]
+----
+@ExtendWith(DbUnitExtension.class)
+class AccountRepositoryTest
+{
+ IDatabaseTester databaseTester =
+ new JdbcDatabaseTester("org.h2.Driver", "jdbc:h2:mem:example;DB_CLOSE_DELAY=-1");
+
+ @BeforeEach
+ void loadDataset() throws Exception
+ {
+ databaseTester.setDataSet(new FlatXmlDataSetBuilder().build(new File("prep.xml")));
+ databaseTester.setTearDownOperation(DatabaseOperation.DELETE_ALL);
+ }
+
+ @Test
+ void testWithdraw_sufficientBalance_decrementsBalance() { ... }
+}
+----
+
+`@BeforeEach` methods still run first — configure the dataset and any
+operation overrides there. The extension then calls `onSetup()` immediately
+before the test method and `onTearDown()` immediately after it, even if the
+test method fails or `onSetup()` itself throws.
+
+== Field Discovery
+
+The extension finds the `IDatabaseTester` by scanning the test instance's
+fields, including inherited ones:
+
+* The nearest declaring class wins — a field on the test class itself takes
+ precedence over one on a superclass.
+* That class must declare exactly one non-static field assignable to
+ `IDatabaseTester`. No match anywhere in the hierarchy, or two-or-more
+ matches at the same class level, both fail fast with a descriptive
+ `IllegalStateException` rather than guessing.
+* Static fields are ignored.
+* Private fields are found; the field's own access modifier doesn't matter.
+
+NOTE: `@Nested` test classes are not supported — field discovery only walks
+the innermost test instance and its superclasses, not enclosing class
+instances, since a Java nested class does not extend its enclosing class.
+
+== When to Use This Instead of Manual Lifecycle Calls
+
+Reach for `DbUnitExtension` when a `@BeforeEach`/`@AfterEach` pair that only
+calls `onSetup()`/`onTearDown()` (as shown in link:IDatabaseTester.html[the
+IDatabaseTester guide]) would just be boilerplate repeated across every test
+class. Write the `@BeforeEach`/`@AfterEach` pair yourself instead when a test
+needs other logic around those calls, or targets a JUnit version this
+extension doesn't support.
diff --git a/src/site/asciidoc/testcases/IDatabaseTester.adoc b/src/site/asciidoc/testcases/IDatabaseTester.adoc
index 26559ffaf..559370cd9 100644
--- a/src/site/asciidoc/testcases/IDatabaseTester.adoc
+++ b/src/site/asciidoc/testcases/IDatabaseTester.adoc
@@ -100,6 +100,11 @@ this lifecycle (plus expected-data verification) behind `runTest()`/
`preTest()`/`postTest()`, and is usually a better starting point than driving
`onSetup()`/`onTearDown()` directly — see its page for the full API.
+TIP: On JUnit 5/6 (Jupiter), link:DbUnitExtension.html[`DbUnitExtension`] can
+call `onSetup()`/`onTearDown()` for you, removing those two calls from the
+`@BeforeEach`/`@AfterEach` pair shown above. Keep `@BeforeEach` if it still
+configures the tester, e.g. via `setDataSet()`/`setTearDownOperation()`.
+
[#IOperationListenerHooks]
== IOperationListener Hooks
diff --git a/src/site/site.xml b/src/site/site.xml
index 9a7b7ee56..f157338e0 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -64,6 +64,7 @@
-
+
diff --git a/src/test/java/org/dbunit/junit/jupiter/DbUnitExtensionLifecycleTest.java b/src/test/java/org/dbunit/junit/jupiter/DbUnitExtensionLifecycleTest.java
new file mode 100644
index 000000000..d407d96a0
--- /dev/null
+++ b/src/test/java/org/dbunit/junit/jupiter/DbUnitExtensionLifecycleTest.java
@@ -0,0 +1,224 @@
+/*
+ *
+ * The DbUnit Database Testing Framework
+ * Copyright (C)2002-2025, 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.dbunit.IOperationListener;
+import org.dbunit.IDatabaseTester;
+import org.dbunit.database.IDatabaseConnection;
+import org.dbunit.dataset.IDataSet;
+import org.dbunit.operation.DatabaseOperation;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.platform.testkit.engine.EngineTestKit;
+
+/**
+ * Lifecycle integration tests for {@link DbUnitExtension} that verify execution
+ * ordering and teardown behavior under failure using {@link EngineTestKit}.
+ */
+class DbUnitExtensionLifecycleTest {
+
+ @Test
+ void testBeforeAndAfterTestExecution_testMethodSucceeds_callbacksRunInOrder() {
+ EngineTestKit.engine("junit-jupiter")
+ .selectors(selectClass(LifecycleOrderSample.class))
+ .execute()
+ .testEvents()
+ .assertStatistics(stats -> stats.started(1).succeeded(1));
+ }
+
+ @Test
+ void testAfterTestExecution_testMethodFails_onTearDownCalled() {
+ FailingTestSample.CALL_LOG.clear();
+
+ EngineTestKit.engine("junit-jupiter")
+ .selectors(selectClass(FailingTestSample.class))
+ .execute()
+ .testEvents()
+ .assertStatistics(stats -> stats.started(1).failed(1));
+
+ assertThat(FailingTestSample.CALL_LOG)
+ .as("onTearDown() must be called even when the test method fails.")
+ .contains("onTearDown");
+ }
+
+ @Test
+ void testBeforeTestExecution_onSetupFails_onTearDownCalledAndTestMethodSkipped() {
+ FailingSetupSample.CALL_LOG.clear();
+
+ EngineTestKit.engine("junit-jupiter")
+ .selectors(selectClass(FailingSetupSample.class))
+ .execute()
+ .testEvents()
+ .assertStatistics(stats -> stats.started(1).failed(1));
+
+ assertThat(FailingSetupSample.CALL_LOG)
+ .as("The test method must not run and onTearDown() must still be called"
+ + " when onSetup() throws.")
+ .containsExactly("onTearDown");
+ }
+
+ /**
+ * Sample test class with a known-good lifecycle that verifies call ordering.
+ * Assertions inside verify that onSetup runs after @BeforeEach and onTearDown
+ * runs before @AfterEach; a failure here surfaces as a test failure in
+ * the outer EngineTestKit assertion.
+ */
+ @ExtendWith(DbUnitExtension.class)
+ static class LifecycleOrderSample {
+ private final List callLog = new ArrayList<>();
+ IDatabaseTester databaseTester = new CallLoggingTester(callLog);
+
+ @BeforeEach
+ void beforeEach() {
+ callLog.add("beforeEach");
+ }
+
+ @AfterEach
+ void afterEach() {
+ assertThat(callLog)
+ .as("Execution order must be: beforeEach, onSetup, test, onTearDown.")
+ .containsExactly("beforeEach", "onSetup", "test", "onTearDown");
+ }
+
+ @Test
+ void testOrderVerification() {
+ assertThat(callLog)
+ .as("onSetup() must run after @BeforeEach and before the test method.")
+ .containsExactly("beforeEach", "onSetup");
+ callLog.add("test");
+ }
+ }
+
+ /**
+ * Sample test class whose single test intentionally fails, used to verify
+ * that onTearDown is still called when the test method throws.
+ */
+ @ExtendWith(DbUnitExtension.class)
+ static class FailingTestSample {
+ static final List CALL_LOG = Collections.synchronizedList(new ArrayList<>());
+ IDatabaseTester databaseTester = new CallLoggingTester(CALL_LOG);
+
+ @Test
+ void testThatFails() {
+ CALL_LOG.add("test");
+ fail("intentional failure to verify onTearDown still runs");
+ }
+ }
+
+ /**
+ * Sample test class whose {@link IDatabaseTester} fails during {@code onSetup()}, used to
+ * verify that {@code onTearDown()} still runs and the test method itself never executes.
+ */
+ @ExtendWith(DbUnitExtension.class)
+ static class FailingSetupSample {
+ static final List CALL_LOG = Collections.synchronizedList(new ArrayList<>());
+ IDatabaseTester databaseTester = new CallLoggingTester(CALL_LOG, true);
+
+ @Test
+ void testThatNeverRuns() {
+ CALL_LOG.add("test");
+ fail("test method must not run when onSetup() throws");
+ }
+ }
+
+ private static class CallLoggingTester implements IDatabaseTester {
+ private final List callLog;
+ private final boolean failOnSetup;
+
+ CallLoggingTester(final List callLog) {
+ this(callLog, false);
+ }
+
+ CallLoggingTester(final List callLog, final boolean failOnSetup) {
+ this.callLog = callLog;
+ this.failOnSetup = failOnSetup;
+ }
+
+ @Override
+ public void onSetup() throws Exception {
+ if (failOnSetup) {
+ throw new Exception("intentional setup failure to verify onTearDown still runs");
+ }
+ callLog.add("onSetup");
+ }
+
+ @Override
+ public void onTearDown() throws Exception {
+ callLog.add("onTearDown");
+ }
+
+ @Override
+ public IDatabaseConnection getConnection() throws Exception {
+ return null;
+ }
+
+ @Override
+ public IDataSet getDataSet() {
+ return null;
+ }
+
+ @Override
+ public void setDataSet(final IDataSet dataSet) {
+ }
+
+ @Override
+ public DatabaseOperation getSetUpOperation() {
+ return null;
+ }
+
+ @Override
+ public DatabaseOperation getTearDownOperation() {
+ return null;
+ }
+
+ @Override
+ public void setSetUpOperation(final DatabaseOperation setUpOperation) {
+ }
+
+ @Override
+ public void setTearDownOperation(final DatabaseOperation tearDownOperation) {
+ }
+
+ @Override
+ public void setOperationListener(final IOperationListener operationListener) {
+ }
+
+ @Override
+ @Deprecated
+ public void closeConnection(final IDatabaseConnection connection) throws Exception {
+ }
+
+ @Override
+ @Deprecated
+ public void setSchema(final String schema) {
+ }
+ }
+}
diff --git a/src/test/java/org/dbunit/junit/jupiter/DbUnitExtensionTest.java b/src/test/java/org/dbunit/junit/jupiter/DbUnitExtensionTest.java
new file mode 100644
index 000000000..392c51b1c
--- /dev/null
+++ b/src/test/java/org/dbunit/junit/jupiter/DbUnitExtensionTest.java
@@ -0,0 +1,286 @@
+/*
+ *
+ * The DbUnit Database Testing Framework
+ * Copyright (C)2002-2025, 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 static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+
+import org.dbunit.IDatabaseTester;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class DbUnitExtensionTest {
+ @Mock
+ ExtensionContext context;
+
+ @Mock
+ ExtensionContext.Store store;
+
+ @Mock
+ IDatabaseTester databaseTester;
+
+ final DbUnitExtension extension = new DbUnitExtension();
+
+ // ---- beforeTestExecution: field discovery happy paths ----
+
+ @Test
+ void testBeforeTestExecution_testerFieldPresent_callsOnSetupAndStoresTester() throws Exception {
+ final HasTester testInstance = new HasTester(databaseTester);
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ extension.beforeTestExecution(context);
+
+ verify(store).put(eq("databaseTester"), eq(databaseTester));
+ verify(databaseTester).onSetup();
+ }
+
+ @Test
+ void testBeforeTestExecution_privateField_callsOnSetup() throws Exception {
+ final HasPrivateTester testInstance = new HasPrivateTester(databaseTester);
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ extension.beforeTestExecution(context);
+
+ verify(databaseTester).onSetup();
+ }
+
+ @Test
+ void testBeforeTestExecution_testerInSuperclass_callsOnSetup() throws Exception {
+ final SubclassOfHasTester testInstance = new SubclassOfHasTester(databaseTester);
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ extension.beforeTestExecution(context);
+
+ verify(databaseTester).onSetup();
+ }
+
+ @Test
+ void testBeforeTestExecution_testerInGrandparentClass_callsOnSetup() throws Exception {
+ final SubSubclassOfHasTester testInstance = new SubSubclassOfHasTester(databaseTester);
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ extension.beforeTestExecution(context);
+
+ verify(databaseTester).onSetup();
+ }
+
+ @Test
+ void testBeforeTestExecution_subclassFieldTakesPrecedenceOverSuperclassField_usesSubclassField()
+ throws Exception {
+ final IDatabaseTester superTester = mock(IDatabaseTester.class);
+ final SubclassWithOwnTester testInstance = new SubclassWithOwnTester(databaseTester, superTester);
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ extension.beforeTestExecution(context);
+
+ verify(databaseTester).onSetup();
+ verify(superTester, never()).onSetup();
+ }
+
+ // ---- beforeTestExecution: error cases ----
+
+ @Test
+ void testBeforeTestExecution_noTestInstance_throwsIllegalStateException() {
+ when(context.getTestInstance()).thenReturn(Optional.empty());
+
+ assertThatThrownBy(() -> extension.beforeTestExecution(context))
+ .as("Expected exception when no test instance is available.")
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("No test instance available");
+ }
+
+ @Test
+ void testBeforeTestExecution_noTesterField_throwsIllegalStateException() {
+ when(context.getTestInstance()).thenReturn(Optional.of(new Object()));
+
+ assertThatThrownBy(() -> extension.beforeTestExecution(context))
+ .as("Expected exception when no IDatabaseTester field is present.")
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("No IDatabaseTester field found");
+ }
+
+ @Test
+ void testBeforeTestExecution_nullTesterField_throwsIllegalStateException() {
+ final HasTester testInstance = new HasTester(null);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ assertThatThrownBy(() -> extension.beforeTestExecution(context))
+ .as("Expected exception when IDatabaseTester field is null.")
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("is null");
+ }
+
+ @Test
+ void testBeforeTestExecution_staticTesterField_throwsIllegalStateException() {
+ when(context.getTestInstance()).thenReturn(Optional.of(new HasStaticTesterOnly()));
+
+ assertThatThrownBy(() -> extension.beforeTestExecution(context))
+ .as("Static IDatabaseTester fields must be skipped.")
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("No IDatabaseTester field found");
+ }
+
+ @Test
+ void testBeforeTestExecution_multipleTesterFieldsInSameClass_throwsIllegalStateException() {
+ final IDatabaseTester otherTester = mock(IDatabaseTester.class);
+ final HasTwoTesters testInstance = new HasTwoTesters(databaseTester, otherTester);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+
+ assertThatThrownBy(() -> extension.beforeTestExecution(context))
+ .as("Expected exception when a class declares more than one IDatabaseTester field.")
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Multiple IDatabaseTester fields found");
+ }
+
+ @Test
+ void testBeforeTestExecution_onSetupThrows_propagatesException() throws Exception {
+ final HasTester testInstance = new HasTester(databaseTester);
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(context.getTestInstance()).thenReturn(Optional.of(testInstance));
+ final Exception cause = new Exception("database setup failed");
+ doThrow(cause).when(databaseTester).onSetup();
+
+ assertThatThrownBy(() -> extension.beforeTestExecution(context))
+ .as("Exception from onSetup() must propagate.")
+ .isSameAs(cause);
+ }
+
+ // ---- afterTestExecution ----
+
+ @Test
+ void testAfterTestExecution_storedTesterPresent_callsOnTearDown() throws Exception {
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(store.get("databaseTester", IDatabaseTester.class)).thenReturn(databaseTester);
+
+ extension.afterTestExecution(context);
+
+ verify(databaseTester).onTearDown();
+ }
+
+ @Test
+ void testAfterTestExecution_noStoredTester_doesNotCallOnTearDown() throws Exception {
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(store.get("databaseTester", IDatabaseTester.class)).thenReturn(null);
+
+ extension.afterTestExecution(context);
+
+ verify(databaseTester, never()).onTearDown();
+ }
+
+ @Test
+ void testAfterTestExecution_onTearDownThrows_propagatesException() throws Exception {
+ when(context.getStore(any(ExtensionContext.Namespace.class))).thenReturn(store);
+ when(store.get("databaseTester", IDatabaseTester.class)).thenReturn(databaseTester);
+ final Exception cause = new Exception("database teardown failed");
+ doThrow(cause).when(databaseTester).onTearDown();
+
+ assertThatThrownBy(() -> extension.afterTestExecution(context))
+ .as("Exception from onTearDown() must propagate.")
+ .isSameAs(cause);
+ }
+
+ // ---- helper inner classes ----
+
+ static class HasTester {
+ IDatabaseTester databaseTester;
+
+ HasTester(final IDatabaseTester databaseTester) {
+ this.databaseTester = databaseTester;
+ }
+ }
+
+ static class HasPrivateTester {
+ private final IDatabaseTester databaseTester;
+
+ HasPrivateTester(final IDatabaseTester databaseTester) {
+ this.databaseTester = databaseTester;
+ }
+ }
+
+ static class HasStaticTesterOnly {
+ @SuppressWarnings("unused")
+ static IDatabaseTester staticTester = null;
+ }
+
+ static class HasTwoTesters {
+ IDatabaseTester databaseTester;
+ IDatabaseTester anotherTester;
+
+ HasTwoTesters(final IDatabaseTester databaseTester, final IDatabaseTester anotherTester) {
+ this.databaseTester = databaseTester;
+ this.anotherTester = anotherTester;
+ }
+ }
+
+ static class ParentHasTester {
+ IDatabaseTester databaseTester;
+
+ ParentHasTester(final IDatabaseTester databaseTester) {
+ this.databaseTester = databaseTester;
+ }
+ }
+
+ static class SubclassOfHasTester extends ParentHasTester {
+ SubclassOfHasTester(final IDatabaseTester databaseTester) {
+ super(databaseTester);
+ }
+ }
+
+ static class SubSubclassOfHasTester extends SubclassOfHasTester {
+ SubSubclassOfHasTester(final IDatabaseTester databaseTester) {
+ super(databaseTester);
+ }
+ }
+
+ static class SuperclassWithTester {
+ IDatabaseTester databaseTester;
+
+ SuperclassWithTester(final IDatabaseTester databaseTester) {
+ this.databaseTester = databaseTester;
+ }
+ }
+
+ static class SubclassWithOwnTester extends SuperclassWithTester {
+ IDatabaseTester databaseTester;
+
+ SubclassWithOwnTester(final IDatabaseTester subTester, final IDatabaseTester superTester) {
+ super(superTester);
+ this.databaseTester = subTester;
+ }
+ }
+}