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.DbUnitExpected;
34  import org.dbunit.annotation.DbUnitPrep;
35  import org.dbunit.annotation.DbUnitRowCountCheck;
36  import org.dbunit.annotation.DbUnitTearDown;
37  import org.dbunit.annotation.DbUnitTester;
38  import org.dbunit.annotation.DbUnitVerifyTable;
39  import org.dbunit.database.IDatabaseConnection;
40  import org.dbunit.database.rowcount.ClearRowCountCheckSystemProperties;
41  import org.dbunit.operation.DbUnitOperation;
42  import org.junit.jupiter.api.Test;
43  import org.junit.jupiter.api.extension.ExtendWith;
44  import org.junit.platform.testkit.engine.EngineTestKit;
45  
46  /**
47   * Real-database integration test that every annotation-driven path leaves the connection ledger
48   * balanced: no connection the run opened is left unclosed, and the runtime never holds more than
49   * its memoized connection plus one the tester opens for an operation. Runs each path through a
50   * {@link DataSourceDatabaseTester} over a {@link CountingDataSource}, which hands out real
51   * connections and tracks open/close and peak concurrency.
52   *
53   * <p>Turns the "delicate connection lifecycle" concern into a per-path assertion (matrix item 9 /
54   * G-c3 companion). Also pins the peak concurrency each path actually reaches:
55   * <ul>
56   * <li>The prep/expected path reaches only <strong>one</strong> - {@code DefaultPrepAndExpectedTestCase}
57   * runs setup, verify and cleanup all on its one reusable connection.</li>
58   * <li>The setup/teardown path reaches <strong>two</strong> - the extension memoizes the
59   * connection {@code onSetup()} retrieves (for a possible row count check baseline or a parameter
60   * injection) and holds it to end of test, so {@code onTearDown()} opening its own makes two.
61   * This holds even with the row count check disabled: the memoize is unconditional on that path.</li>
62   * </ul>
63   */
64  @ClearRowCountCheckSystemProperties
65  class DbUnitExtensionConnectionBalanceIT
66  {
67      private static final String TEST_TABLE = "TEST_TABLE";
68  
69      @Test
70      void testAfterTestExecution_setupTeardownPathNoRowCountCheck_balancedPeakOfTwo()
71              throws Exception
72      {
73          runBalanced(SetupTeardownNoCheckSample.class, 2);
74      }
75  
76      @Test
77      void testAfterTestExecution_setupTeardownPathWithRowCountCheck_balancedPeakOfTwo()
78              throws Exception
79      {
80          runBalanced(SetupTeardownWithCheckSample.class, 2);
81      }
82  
83      @Test
84      void testAfterTestExecution_prepExpectedPathWithRowCountCheck_balancedPeakOfOne()
85              throws Exception
86      {
87          runBalanced(PrepExpectedWithCheckSample.class, 1);
88      }
89  
90      @Test
91      void testAfterTestExecution_injectedConnectionParameterWithRowCountCheck_balancedPeakOfTwo()
92              throws Exception
93      {
94          runBalanced(InjectedConnectionSample.class, 2);
95      }
96  
97      private void runBalanced(final Class<?> sampleClass, final int expectedPeak) throws Exception
98      {
99          final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
100         final CountingDataSource dataSource = new CountingDataSource(environment.getProfile());
101         deleteAllRowsQuietly(environment);
102         BalanceSample.databaseTester = new DataSourceDatabaseTester(dataSource,
103                 environment.getProfile().getSchema());
104         try
105         {
106             EngineTestKit.engine("junit-jupiter").selectors(selectClass(sampleClass)).execute()
107                     .testEvents().assertStatistics(stats -> stats.started(1).succeeded(1));
108 
109             assertThat(dataSource.leaked())
110                     .as("%s must close every connection it opened.", sampleClass.getSimpleName())
111                     .isZero();
112             assertThat(dataSource.peakConcurrent())
113                     .as("%s reaches this peak connection concurrency - see the class Javadoc.",
114                             sampleClass.getSimpleName())
115                     .isEqualTo(expectedPeak);
116         } finally
117         {
118             deleteAllRowsQuietly(environment);
119             environment.closeConnection();
120         }
121     }
122 
123     private static void deleteAllRowsQuietly(final DatabaseEnvironment environment)
124     {
125         try (Statement statement =
126                 environment.getConnection().getConnection().createStatement())
127         {
128             statement.execute("DELETE FROM " + TEST_TABLE);
129         } catch (final Exception e)
130         {
131             // best-effort cleanup only; a failure here must not fail the test that already ran
132         }
133     }
134 
135     abstract static class BalanceSample
136     {
137         @DbUnitTester
138         static IDatabaseTester databaseTester;
139     }
140 
141     @ExtendWith(DbUnitExtension.class)
142     @ClearRowCountCheckSystemProperties
143     static class SetupTeardownNoCheckSample extends BalanceSample
144     {
145         @Test
146         @DbUnitPrep("annotation-it-prep.xml")
147         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
148         void runsTheLifecycle()
149         {
150         }
151     }
152 
153     @ExtendWith(DbUnitExtension.class)
154     @ClearRowCountCheckSystemProperties
155     @DbUnitRowCountCheck
156     static class SetupTeardownWithCheckSample extends BalanceSample
157     {
158         @Test
159         @DbUnitPrep("annotation-it-prep.xml")
160         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
161         void runsTheLifecycle()
162         {
163         }
164     }
165 
166     @ExtendWith(DbUnitExtension.class)
167     @ClearRowCountCheckSystemProperties
168     @DbUnitRowCountCheck
169     static class PrepExpectedWithCheckSample extends BalanceSample
170     {
171         @Test
172         @DbUnitPrep("annotation-it-prep.xml")
173         @DbUnitExpected(value = "annotation-it-expected.xml",
174                 verify = @DbUnitVerifyTable(value = TEST_TABLE,
175                         include = {"COLUMN0", "COLUMN1"}))
176         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
177         void mutateTheSeededRow(final Connection connection) throws Exception
178         {
179             try (Statement statement = connection.createStatement())
180             {
181                 statement.execute("UPDATE " + TEST_TABLE
182                         + " SET COLUMN1 = 'after' WHERE COLUMN0 = 'row0'");
183             }
184         }
185     }
186 
187     @ExtendWith(DbUnitExtension.class)
188     @ClearRowCountCheckSystemProperties
189     @DbUnitRowCountCheck
190     static class InjectedConnectionSample extends BalanceSample
191     {
192         @Test
193         @DbUnitPrep("annotation-it-prep.xml")
194         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
195         void readsThroughTheInjectedConnection(final IDatabaseConnection connection)
196                 throws Exception
197         {
198             try (Statement statement = connection.getConnection().createStatement();
199                     ResultSet resultSet = statement.executeQuery(
200                             "SELECT COUNT(*) FROM " + TEST_TABLE + " WHERE COLUMN0 = 'row0'"))
201             {
202                 resultSet.next();
203                 assertThat(resultSet.getInt(1))
204                         .as("The injected connection must see @DbUnitPrep's seeded row.")
205                         .isEqualTo(1);
206             }
207         }
208     }
209 }