View Javadoc
1   /*
2    *
3    * The DbUnit Database Testing Framework
4    * Copyright (C)2002-2026, DbUnit.org
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 2.1 of the License, or (at your option) any later version.
10   *
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, write to the Free Software
18   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19   *
20   */
21  package org.dbunit.junit.jupiter;
22  
23  import static org.assertj.core.api.Assertions.assertThat;
24  import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
25  
26  import java.sql.Connection;
27  import java.sql.ResultSet;
28  import java.sql.Statement;
29  import java.util.ArrayList;
30  import java.util.List;
31  
32  import org.dbunit.DatabaseEnvironment;
33  import org.dbunit.DatabaseProfile;
34  import org.dbunit.DatabaseTesterFactory;
35  import org.dbunit.DefaultPrepAndExpectedTestCase;
36  import org.dbunit.IDatabaseTester;
37  import org.dbunit.JdbcDatabaseTester;
38  import org.dbunit.PrepAndExpectedTestCase;
39  import org.dbunit.annotation.DbUnitConfig;
40  import org.dbunit.annotation.DbUnitExpected;
41  import org.dbunit.annotation.DbUnitPrep;
42  import org.dbunit.annotation.DbUnitRowCountCheck;
43  import org.dbunit.annotation.DbUnitTearDown;
44  import org.dbunit.annotation.DbUnitTestCase;
45  import org.dbunit.annotation.DbUnitVerifyTable;
46  import org.dbunit.database.CachingConnectionProvider;
47  import org.dbunit.database.IDatabaseConnection;
48  import org.dbunit.database.rowcount.ClearRowCountCheckSystemProperties;
49  import org.dbunit.operation.DbUnitOperation;
50  import org.junit.jupiter.api.AfterEach;
51  import org.junit.jupiter.api.RepeatedTest;
52  import org.junit.jupiter.api.Test;
53  import org.junit.jupiter.api.extension.ExtendWith;
54  import org.junit.platform.testkit.engine.EngineTestKit;
55  
56  /**
57   * Real-database (hsqldb) integration test of the annotation-driven prep/expected path under the
58   * exact combination that broke a downstream build three separate ways (issues #962/#964/#965):
59   * a single {@link DefaultPrepAndExpectedTestCase} reused across test methods through a
60   * {@code @DbUnitTestCase} field, its tester built over a shared {@link CachingConnectionProvider},
61   * {@code @DbUnitConfig(closeConnectionAfterTest = false)} so the connection is pinned across
62   * methods, and {@code @DbUnitRowCountCheck} active - then the pinned connection is force-closed
63   * between two methods, as a pool max-lifetime reap or a database idle-in-transaction timeout
64   * would do.
65   *
66   * <p>The mocked tester in {@code AnnotatedTestExecutorTest} and the direct-drive
67   * {@code DatabaseTesterConnectionReuseIT} (which never goes through {@link DbUnitExtension}) cannot
68   * prove the annotation layer stacked on top of that machinery also recovers. This does:
69   * every repetition after the kill must re-acquire a live connection rather than fail on the dead
70   * one, and every repetition's {@code @DbUnitTearDown} must have actually committed.
71   *
72   * <p>Matrix rows 8 + G-c5 (see {@code plan-docs/annotation-branch-connection-matrix.adoc}).
73   */
74  class DbUnitExtensionConnectionReuseIT
75  {
76      private static final String TEST_TABLE = "TEST_TABLE";
77  
78      @Test
79      void testAfterTestExecution_reusedInjectedTestCaseWhoseCachedConnectionDiesMidRun_reacquiresAndKeepsPassing()
80              throws Exception
81      {
82          final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
83          final CachingConnectionProvider provider = new CachingConnectionProvider();
84          deleteAllRowsQuietly(environment);
85          ReusedInstanceSample.profile = environment.getProfile();
86          ReusedInstanceSample.provider = provider;
87          ReusedInstanceSample.reset();
88          try
89          {
90              EngineTestKit.engine("junit-jupiter")
91                      .selectors(selectClass(ReusedInstanceSample.class)).execute().testEvents()
92                      .assertStatistics(stats -> stats.started(5).succeeded(5));
93  
94              final List<Connection> seen = ReusedInstanceSample.rawConnectionsSeen;
95              assertThat(seen)
96                      .as("Every one of the 5 repetitions must have run, including the three"
97                              + " after the pinned connection was force-closed between #2 and"
98                              + " #3 - proving the annotation path re-acquires a dropped"
99                              + " connection (#962) rather than reusing the dead one or failing"
100                             + " in preTest()/setupData().")
101                     .hasSize(5);
102             assertThat(seen.subList(0, 2))
103                     .as("Repetitions before the kill share the one connection the"
104                             + " CachingConnectionProvider cached and closeConnectionAfterTest=false"
105                             + " pinned.")
106                     .containsOnly(seen.get(0));
107             assertThat(seen.subList(2, 5))
108                     .as("Repetitions after the kill share a single replacement connection - one"
109                             + " re-acquisition, not a fresh connection per method.")
110                     .containsOnly(seen.get(2));
111             assertThat(seen.get(0))
112                     .as("The replacement must be a genuinely different physical connection, not"
113                             + " the dead one handed back again.")
114                     .isNotSameAs(seen.get(2));
115 
116             final IDatabaseConnection verifyConnection = environment.getConnection();
117             assertThat(rowCount(verifyConnection, TEST_TABLE))
118                     .as("Each repetition's @DbUnitTearDown(DELETE_ALL) must have committed:"
119                             + " a separate connection must see TEST_TABLE empty after the run,"
120                             + " not the rows a never-committed teardown would leave behind"
121                             + " (#965).")
122                     .isZero();
123         } finally
124         {
125             deleteAllRowsQuietly(environment);
126             provider.close();
127             environment.closeConnection();
128         }
129     }
130 
131     private static int rowCount(final IDatabaseConnection connection, final String tableName)
132             throws Exception
133     {
134         try (Statement statement = connection.getConnection().createStatement();
135                 ResultSet resultSet =
136                         statement.executeQuery("SELECT COUNT(*) FROM " + tableName))
137         {
138             resultSet.next();
139             return resultSet.getInt(1);
140         }
141     }
142 
143     private static void deleteAllRowsQuietly(final DatabaseEnvironment environment)
144     {
145         try (Statement statement =
146                 environment.getConnection().getConnection().createStatement())
147         {
148             statement.execute("DELETE FROM " + TEST_TABLE);
149         } catch (final Exception e)
150         {
151             // best-effort cleanup only; a failure here must not fail the test that already ran
152         }
153     }
154 
155     @ExtendWith(DbUnitExtension.class)
156     @ClearRowCountCheckSystemProperties
157     @DbUnitConfig(closeConnectionAfterTest = false,
158             databaseTesterFactory = ReusedInstanceSample.SharedProviderTesterFactory.class)
159     @DbUnitRowCountCheck
160     static class ReusedInstanceSample
161     {
162         static DatabaseProfile profile;
163         static CachingConnectionProvider provider;
164         static final List<Connection> rawConnectionsSeen = new ArrayList<>();
165         static int repetitionsCompleted;
166 
167         // One instance for the whole class - the reuse-across-methods scenario #962 is about.
168         // The extension resolves its tester once (getDatabaseTester() is null the first
169         // repetition, non-null afterward), so all repetitions share the one tester and the one
170         // CachingConnectionProvider behind it.
171         @DbUnitTestCase
172         static final PrepAndExpectedTestCase testCase = new DefaultPrepAndExpectedTestCase();
173 
174         static void reset()
175         {
176             rawConnectionsSeen.clear();
177             repetitionsCompleted = 0;
178         }
179 
180         @RepeatedTest(5)
181         @DbUnitPrep("annotation-it-prep.xml")
182         @DbUnitExpected(value = "annotation-it-expected.xml",
183                 verify = @DbUnitVerifyTable(value = TEST_TABLE,
184                         include = {"COLUMN0", "COLUMN1"}))
185         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
186         void mutateTheSeededRow() throws Exception
187         {
188             final IDatabaseConnection connection = reusableConnection();
189             rawConnectionsSeen.add(connection.getConnection());
190             try (Statement statement = connection.getConnection().createStatement())
191             {
192                 statement.execute("UPDATE " + TEST_TABLE
193                         + " SET COLUMN1 = 'after' WHERE COLUMN0 = 'row0'");
194             }
195         }
196 
197         @AfterEach
198         void dropTheCachedConnectionBetweenTheSecondAndThirdRepetition() throws Exception
199         {
200             repetitionsCompleted++;
201             if (repetitionsCompleted == 2)
202             {
203                 // The 2nd repetition has fully finished - its @DbUnitExpected verify,
204                 // @DbUnitTearDown(DELETE_ALL) and @DbUnitRowCountCheck all ran. Now the pool or
205                 // the server drops the idle connection, exactly as an Agroal max-lifetime reap
206                 // or a PostgreSQL idle_in_transaction_session_timeout would.
207                 reusableConnection().getConnection().close();
208             }
209         }
210 
211         private static IDatabaseConnection reusableConnection() throws Exception
212         {
213             return ((DefaultPrepAndExpectedTestCase) testCase).getReusableConnection();
214         }
215 
216         static class SharedProviderTesterFactory implements DatabaseTesterFactory
217         {
218             @Override
219             public IDatabaseTester getDatabaseTester() throws Exception
220             {
221                 return new JdbcDatabaseTester(profile.getDriverClass(),
222                         profile.getConnectionUrl(), profile.getUser(), profile.getPassword(),
223                         profile.getSchema(), provider);
224             }
225         }
226     }
227 }