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  
30  import org.dbunit.DataSourceDatabaseTester;
31  import org.dbunit.DatabaseEnvironment;
32  import org.dbunit.IDatabaseTester;
33  import org.dbunit.annotation.DbUnitConfig;
34  import org.dbunit.annotation.DbUnitExpected;
35  import org.dbunit.annotation.DbUnitPrep;
36  import org.dbunit.annotation.DbUnitProperty;
37  import org.dbunit.annotation.DbUnitRowCountCheck;
38  import org.dbunit.annotation.DbUnitTearDown;
39  import org.dbunit.annotation.DbUnitTester;
40  import org.dbunit.annotation.DbUnitVerifyTable;
41  import org.dbunit.database.IDatabaseConnection;
42  import org.dbunit.database.rowcount.ClearRowCountCheckSystemProperties;
43  import org.dbunit.operation.DbUnitOperation;
44  import org.junit.jupiter.api.BeforeEach;
45  import org.junit.jupiter.api.Test;
46  import org.junit.jupiter.api.extension.ExtendWith;
47  import org.junit.platform.testkit.engine.EngineTestKit;
48  
49  /**
50   * Real-database integration test that every annotation-driven path leaves the connection ledger
51   * balanced: no connection the run opened is left unclosed, and the runtime never holds more than
52   * its memoized connection plus one the tester opens for an operation. Runs each path through a
53   * {@link DataSourceDatabaseTester} over a {@link CountingDataSource}, which hands out real
54   * connections and tracks open/close and peak concurrency.
55   *
56   * <p>Turns the "delicate connection lifecycle" concern into a per-path assertion (matrix item 9 /
57   * G-c3 companion). Also pins the peak concurrency each path actually reaches:
58   * <ul>
59   * <li>The prep/expected path reaches only <strong>one</strong> - {@code DefaultPrepAndExpectedTestCase}
60   * runs setup, verify and cleanup all on its one reusable connection.</li>
61   * <li>The setup/teardown path reaches <strong>two</strong> - the extension memoizes the
62   * connection {@code onSetup()} retrieves (for a possible row count check baseline or a parameter
63   * injection) and holds it to end of test, so {@code onTearDown()} opening its own makes two.
64   * This holds even with the row count check disabled: the memoize is unconditional on that path.</li>
65   * </ul>
66   */
67  @ClearRowCountCheckSystemProperties
68  class DbUnitExtensionConnectionBalanceIT
69  {
70      private static final String TEST_TABLE = "TEST_TABLE";
71  
72      @Test
73      void testAfterTestExecution_setupTeardownPathNoRowCountCheck_balancedPeakOfTwo()
74              throws Exception
75      {
76          runBalanced(SetupTeardownNoCheckSample.class, 2);
77      }
78  
79      @Test
80      void testAfterTestExecution_setupTeardownPathWithRowCountCheck_balancedPeakOfTwo()
81              throws Exception
82      {
83          runBalanced(SetupTeardownWithCheckSample.class, 2);
84      }
85  
86      @Test
87      void testAfterTestExecution_prepExpectedPathWithRowCountCheck_balancedPeakOfOne()
88              throws Exception
89      {
90          runBalanced(PrepExpectedWithCheckSample.class, 1);
91      }
92  
93      @Test
94      void testAfterTestExecution_injectedConnectionParameterWithRowCountCheck_balancedPeakOfTwo()
95              throws Exception
96      {
97          runBalanced(InjectedConnectionSample.class, 2);
98      }
99  
100     @Test
101     void testAfterEach_beforeEachFailsAfterConnectionInjected_closesEveryConnectionOpened()
102             throws Exception
103     {
104         final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
105         final CountingDataSource dataSource = new CountingDataSource(environment.getProfile());
106         BalanceSample.databaseTester = new DataSourceDatabaseTester(dataSource,
107                 environment.getProfile().getSchema());
108         try
109         {
110             EngineTestKit.engine("junit-jupiter")
111                     .selectors(selectClass(FailingBeforeEachSample.class)).execute().testEvents()
112                     .assertStatistics(stats -> stats.started(1).failed(1));
113 
114             assertThat(dataSource.opened())
115                     .as("The failing @BeforeEach must have been handed a connection, or this"
116                             + " test proves nothing.")
117                     .isPositive();
118             assertThat(dataSource.leaked())
119                     .as("A connection injected into a @BeforeEach that then fails must still be"
120                             + " closed, because no after-test callback runs once @BeforeEach"
121                             + " fails.")
122                     .isZero();
123         } finally
124         {
125             environment.closeConnection();
126         }
127     }
128 
129     @Test
130     void testBeforeTestExecution_invalidPropertyValueThrowsInTheListener_closesTheTestersConnection()
131             throws Exception
132     {
133         final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
134         final CountingDataSource dataSource = new CountingDataSource(environment.getProfile());
135         BalanceSample.databaseTester = new DataSourceDatabaseTester(dataSource,
136                 environment.getProfile().getSchema());
137         try
138         {
139             EngineTestKit.engine("junit-jupiter")
140                     .selectors(selectClass(InvalidPropertyValueSample.class)).execute()
141                     .testEvents().assertStatistics(stats -> stats.started(1).failed(1));
142 
143             assertThat(dataSource.opened())
144                     .as("onSetup() must have opened a connection, or this test proves nothing.")
145                     .isPositive();
146             assertThat(dataSource.leaked())
147                     .as("A connection the tester opened must be closed even when the executor's"
148                             + " listener throws while applying a @DbUnitProperty value to it.")
149                     .isZero();
150         } finally
151         {
152             environment.closeConnection();
153         }
154     }
155 
156     @Test
157     void testBeforeTestExecution_invalidPropertyValueOnTheExpectedPath_closesTheTestersConnection()
158             throws Exception
159     {
160         final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
161         final CountingDataSource dataSource = new CountingDataSource(environment.getProfile());
162         BalanceSample.databaseTester = new DataSourceDatabaseTester(dataSource,
163                 environment.getProfile().getSchema());
164         try
165         {
166             EngineTestKit.engine("junit-jupiter")
167                     .selectors(selectClass(InvalidPropertyValueOnExpectedPathSample.class))
168                     .execute().testEvents()
169                     .assertStatistics(stats -> stats.started(1).failed(1));
170 
171             assertThat(dataSource.opened())
172                     .as("configureTest() must have opened a connection, or this test proves"
173                             + " nothing.")
174                     .isPositive();
175             assertThat(dataSource.leaked())
176                     .as("A connection the tester opened must be closed even when applying a"
177                             + " @DbUnitProperty value to it fails on the prep/expected path.")
178                     .isZero();
179         } finally
180         {
181             environment.closeConnection();
182         }
183     }
184 
185     @Test
186     void testBeforeTestExecution_expectedFileMissing_closesTheConnectionConfigureTestAcquired()
187             throws Exception
188     {
189         final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
190         final CountingDataSource dataSource = new CountingDataSource(environment.getProfile());
191         BalanceSample.databaseTester = new DataSourceDatabaseTester(dataSource,
192                 environment.getProfile().getSchema());
193         try
194         {
195             EngineTestKit.engine("junit-jupiter")
196                     .selectors(selectClass(MissingExpectedFileSample.class)).execute()
197                     .testEvents().assertStatistics(stats -> stats.started(1).failed(1));
198 
199             assertThat(dataSource.opened())
200                     .as("configureTest() must have opened a connection, or this test proves"
201                             + " nothing.")
202                     .isPositive();
203             assertThat(dataSource.leaked())
204                     .as("A connection configureTest() acquired must be closed when it then fails"
205                             + " to load the expected data file, because the runtime skips"
206                             + " postTest() for a test that never finished configuring.")
207                     .isZero();
208         } finally
209         {
210             environment.closeConnection();
211         }
212     }
213 
214     private void runBalanced(final Class<?> sampleClass, final int expectedPeak) throws Exception
215     {
216         final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
217         final CountingDataSource dataSource = new CountingDataSource(environment.getProfile());
218         deleteAllRowsQuietly(environment);
219         BalanceSample.databaseTester = new DataSourceDatabaseTester(dataSource,
220                 environment.getProfile().getSchema());
221         try
222         {
223             EngineTestKit.engine("junit-jupiter").selectors(selectClass(sampleClass)).execute()
224                     .testEvents().assertStatistics(stats -> stats.started(1).succeeded(1));
225 
226             assertThat(dataSource.leaked())
227                     .as("%s must close every connection it opened.", sampleClass.getSimpleName())
228                     .isZero();
229             assertThat(dataSource.peakConcurrent())
230                     .as("%s reaches this peak connection concurrency - see the class Javadoc.",
231                             sampleClass.getSimpleName())
232                     .isEqualTo(expectedPeak);
233         } finally
234         {
235             deleteAllRowsQuietly(environment);
236             environment.closeConnection();
237         }
238     }
239 
240     private static void deleteAllRowsQuietly(final DatabaseEnvironment environment)
241     {
242         try (Statement statement =
243                 environment.getConnection().getConnection().createStatement())
244         {
245             statement.execute("DELETE FROM " + TEST_TABLE);
246         } catch (final Exception e)
247         {
248             // best-effort cleanup only; a failure here must not fail the test that already ran
249         }
250     }
251 
252     abstract static class BalanceSample
253     {
254         @DbUnitTester
255         static IDatabaseTester databaseTester;
256     }
257 
258     @ExtendWith(DbUnitExtension.class)
259     @ClearRowCountCheckSystemProperties
260     static class SetupTeardownNoCheckSample extends BalanceSample
261     {
262         @Test
263         @DbUnitPrep("annotation-it-prep.xml")
264         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
265         void runsTheLifecycle()
266         {
267         }
268     }
269 
270     @ExtendWith(DbUnitExtension.class)
271     @ClearRowCountCheckSystemProperties
272     @DbUnitRowCountCheck
273     static class SetupTeardownWithCheckSample extends BalanceSample
274     {
275         @Test
276         @DbUnitPrep("annotation-it-prep.xml")
277         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
278         void runsTheLifecycle()
279         {
280         }
281     }
282 
283     @ExtendWith(DbUnitExtension.class)
284     @ClearRowCountCheckSystemProperties
285     @DbUnitRowCountCheck
286     static class PrepExpectedWithCheckSample extends BalanceSample
287     {
288         @Test
289         @DbUnitPrep("annotation-it-prep.xml")
290         @DbUnitExpected(value = "annotation-it-expected.xml",
291                 verify = @DbUnitVerifyTable(value = TEST_TABLE,
292                         include = {"COLUMN0", "COLUMN1"}))
293         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
294         void mutateTheSeededRow(final Connection connection) throws Exception
295         {
296             try (Statement statement = connection.createStatement())
297             {
298                 statement.execute("UPDATE " + TEST_TABLE
299                         + " SET COLUMN1 = 'after' WHERE COLUMN0 = 'row0'");
300             }
301         }
302     }
303 
304     @ExtendWith(DbUnitExtension.class)
305     @ClearRowCountCheckSystemProperties
306     @DbUnitRowCountCheck
307     static class InjectedConnectionSample extends BalanceSample
308     {
309         @Test
310         @DbUnitPrep("annotation-it-prep.xml")
311         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
312         void readsThroughTheInjectedConnection(final IDatabaseConnection connection)
313                 throws Exception
314         {
315             try (Statement statement = connection.getConnection().createStatement();
316                     ResultSet resultSet = statement.executeQuery(
317                             "SELECT COUNT(*) FROM " + TEST_TABLE + " WHERE COLUMN0 = 'row0'"))
318             {
319                 resultSet.next();
320                 assertThat(resultSet.getInt(1))
321                         .as("The injected connection must see @DbUnitPrep's seeded row.")
322                         .isEqualTo(1);
323             }
324         }
325     }
326 
327     @ExtendWith(DbUnitExtension.class)
328     @ClearRowCountCheckSystemProperties
329     static class FailingBeforeEachSample extends BalanceSample
330     {
331         @BeforeEach
332         void failAfterTheConnectionIsInjected(final Connection connection)
333         {
334             throw new IllegalStateException("intentional @BeforeEach failure");
335         }
336 
337         @Test
338         @DbUnitPrep("annotation-it-prep.xml")
339         void neverRuns()
340         {
341         }
342     }
343 
344     @ExtendWith(DbUnitExtension.class)
345     @ClearRowCountCheckSystemProperties
346     @DbUnitConfig(properties = @DbUnitProperty(name = "batchSize", value = "notANumber"))
347     static class InvalidPropertyValueSample extends BalanceSample
348     {
349         @Test
350         @DbUnitPrep("annotation-it-prep.xml")
351         void neverRuns()
352         {
353         }
354     }
355 
356     @ExtendWith(DbUnitExtension.class)
357     @ClearRowCountCheckSystemProperties
358     @DbUnitConfig(properties = @DbUnitProperty(name = "batchSize", value = "notANumber"))
359     static class InvalidPropertyValueOnExpectedPathSample extends BalanceSample
360     {
361         @Test
362         @DbUnitPrep("annotation-it-prep.xml")
363         @DbUnitExpected(value = "annotation-it-expected.xml",
364                 verify = @DbUnitVerifyTable(value = TEST_TABLE,
365                         include = {"COLUMN0", "COLUMN1"}))
366         void neverRuns()
367         {
368         }
369     }
370 
371     @ExtendWith(DbUnitExtension.class)
372     @ClearRowCountCheckSystemProperties
373     static class MissingExpectedFileSample extends BalanceSample
374     {
375         @Test
376         @DbUnitPrep("annotation-it-prep.xml")
377         @DbUnitExpected(value = "no-such-expected-file.xml",
378                 verify = @DbUnitVerifyTable(value = TEST_TABLE,
379                         include = {"COLUMN0", "COLUMN1"}))
380         void neverRuns()
381         {
382         }
383     }
384 }