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.DriverManager;
28  import java.sql.ResultSet;
29  import java.sql.Statement;
30  
31  import org.dbunit.DatabaseEnvironment;
32  import org.dbunit.DatabaseProfile;
33  import org.dbunit.DefaultDatabaseTester;
34  import org.dbunit.IDatabaseTester;
35  import org.dbunit.IOperationListener;
36  import org.dbunit.annotation.DbUnitExpected;
37  import org.dbunit.annotation.DbUnitPrep;
38  import org.dbunit.annotation.DbUnitRowCountCheck;
39  import org.dbunit.annotation.DbUnitTearDown;
40  import org.dbunit.annotation.DbUnitTester;
41  import org.dbunit.annotation.DbUnitVerifyTable;
42  import org.dbunit.database.DatabaseConnection;
43  import org.dbunit.database.IDatabaseConnection;
44  import org.dbunit.database.rowcount.ClearRowCountCheckSystemProperties;
45  import org.dbunit.operation.DbUnitOperation;
46  import org.junit.jupiter.api.Test;
47  import org.junit.jupiter.api.extension.ExtendWith;
48  import org.junit.platform.testkit.engine.EngineTestKit;
49  import org.slf4j.LoggerFactory;
50  
51  import ch.qos.logback.classic.Level;
52  import ch.qos.logback.classic.Logger;
53  import ch.qos.logback.classic.spi.ILoggingEvent;
54  import ch.qos.logback.core.read.ListAppender;
55  
56  /**
57   * Real-database integration test of the annotation prep/expected path when it is handed a
58   * connection with autocommit disabled (issue #965). {@code DefaultPrepAndExpectedTestCase}'s
59   * setup/teardown operations do not manage a transaction, so their writes are never committed;
60   * the row count check's own read transactions are still rolled back so the run completes and
61   * the connection is not left idle-in-transaction (issue #964). This proves both, end to end
62   * through {@link DbUnitExtension}: the warning fires, the check-driven run finishes clean, and
63   * nothing the test wrote survives to a separate connection.
64   *
65   * <p>{@code DefaultPrepAndExpectedTestCaseTest} covers the warning itself with a mocked
66   * connection; only a real database shows the "writes never persist" consequence.
67   *
68   * <p>Matrix modifier: autocommit-off across the prep/expected rows (G-c4).
69   */
70  @ClearRowCountCheckSystemProperties
71  class DbUnitExtensionAutoCommitOffIT
72  {
73      private static final String TEST_TABLE = "TEST_TABLE";
74  
75      @Test
76      void testAfterTestExecution_prepExpectedRowCountCheckOnANonAutocommitConnection_warnsRunsCleanAndPersistsNothing()
77              throws Exception
78      {
79          final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
80          final DatabaseProfile profile = environment.getProfile();
81          deleteAllRowsQuietly(environment);
82  
83          final Connection nonAutocommit = DriverManager.getConnection(
84                  profile.getConnectionUrl(), profile.getUser(), profile.getPassword());
85          nonAutocommit.setAutoCommit(false);
86          AutoCommitOffSample.databaseTester = new DefaultDatabaseTester(
87                  new DatabaseConnection(nonAutocommit, profile.getSchema()));
88          AutoCommitOffSample.databaseTester
89                  .setOperationListener(IOperationListener.NO_OP_OPERATION_LISTENER);
90  
91          final Logger warningLogger = (Logger) LoggerFactory
92                  .getLogger("org.dbunit.database.connection.AutoCommitOffWarning");
93          final ListAppender<ILoggingEvent> appender = new ListAppender<>();
94          appender.start();
95          warningLogger.addAppender(appender);
96          try
97          {
98              EngineTestKit.engine("junit-jupiter")
99                      .selectors(selectClass(AutoCommitOffSample.class)).execute().testEvents()
100                     .assertStatistics(stats -> stats.started(1).succeeded(1));
101 
102             assertThat(appender.list)
103                     .filteredOn(event -> event.getLevel() == Level.WARN
104                             && event.getFormattedMessage().contains("autocommit disabled"))
105                     .as("A non-autocommit connection on the prep/expected path must be warned"
106                             + " about (#965).")
107                     .hasSize(1);
108 
109             final IDatabaseConnection verifyConnection = environment.getConnection();
110             assertThat(rowCount(verifyConnection, TEST_TABLE))
111                     .as("With autocommit disabled the setup/teardown operations never commit"
112                             + " (#965), so a separate connection must see TEST_TABLE unchanged"
113                             + " from before the run - the @DbUnitPrep seed and the test's own"
114                             + " UPDATE both invisible outside the never-committed transaction.")
115                     .isZero();
116         } finally
117         {
118             warningLogger.detachAppender(appender);
119             appender.stop();
120             closeQuietly(nonAutocommit);
121             deleteAllRowsQuietly(environment);
122             environment.closeConnection();
123         }
124     }
125 
126     @Test
127     void testBeforeTestExecution_setupTeardownPrepOnANonAutocommitConnection_warnsAndPersistsNothing()
128             throws Exception
129     {
130         final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
131         final DatabaseProfile profile = environment.getProfile();
132         deleteAllRowsQuietly(environment);
133 
134         final Connection nonAutocommit = DriverManager.getConnection(
135                 profile.getConnectionUrl(), profile.getUser(), profile.getPassword());
136         nonAutocommit.setAutoCommit(false);
137         AutoCommitOffSetupTeardownSample.databaseTester = new DefaultDatabaseTester(
138                 new DatabaseConnection(nonAutocommit, profile.getSchema()));
139 
140         final Logger warningLogger = (Logger) LoggerFactory
141                 .getLogger("org.dbunit.database.connection.AutoCommitOffWarning");
142         final ListAppender<ILoggingEvent> appender = new ListAppender<>();
143         appender.start();
144         warningLogger.addAppender(appender);
145         try
146         {
147             EngineTestKit.engine("junit-jupiter")
148                     .selectors(selectClass(AutoCommitOffSetupTeardownSample.class)).execute()
149                     .testEvents().assertStatistics(stats -> stats.started(1).succeeded(1));
150 
151             assertThat(appender.list)
152                     .filteredOn(event -> event.getLevel() == Level.WARN
153                             && event.getFormattedMessage().contains("autocommit disabled"))
154                     .as("The setup/teardown path must warn about a non-autocommit connection"
155                             + " too - its @DbUnitPrep runs through the tester with no"
156                             + " transaction, exactly the #965 shape the prep/expected path"
157                             + " already warns about.")
158                     .hasSize(1);
159 
160             // The extension closes the tester's connection at end of test (default
161             // closeConnectionAfterTest, no non-closing listener), which rolls the
162             // never-committed @DbUnitPrep back, so a separate connection sees no rows.
163             final IDatabaseConnection verifyConnection = environment.getConnection();
164             assertThat(rowCount(verifyConnection, TEST_TABLE))
165                     .as("The @DbUnitPrep CLEAN_INSERT ran on the never-committed transaction, so"
166                             + " a separate connection sees no rows.")
167                     .isZero();
168         } finally
169         {
170             warningLogger.detachAppender(appender);
171             appender.stop();
172             closeQuietly(nonAutocommit);
173             deleteAllRowsQuietly(environment);
174             environment.closeConnection();
175         }
176     }
177 
178     private static int rowCount(final IDatabaseConnection connection, final String tableName)
179             throws Exception
180     {
181         try (Statement statement = connection.getConnection().createStatement();
182                 ResultSet resultSet =
183                         statement.executeQuery("SELECT COUNT(*) FROM " + tableName))
184         {
185             resultSet.next();
186             return resultSet.getInt(1);
187         }
188     }
189 
190     private static void closeQuietly(final Connection connection)
191     {
192         try
193         {
194             if (!connection.isClosed())
195             {
196                 connection.rollback();
197                 connection.close();
198             }
199         } catch (final Exception e)
200         {
201             // best-effort
202         }
203     }
204 
205     private static void deleteAllRowsQuietly(final DatabaseEnvironment environment)
206     {
207         try (Statement statement =
208                 environment.getConnection().getConnection().createStatement())
209         {
210             statement.execute("DELETE FROM " + TEST_TABLE);
211         } catch (final Exception e)
212         {
213             // best-effort cleanup only; a failure here must not fail the test that already ran
214         }
215     }
216 
217     @ExtendWith(DbUnitExtension.class)
218     @ClearRowCountCheckSystemProperties
219     @DbUnitRowCountCheck
220     static class AutoCommitOffSample
221     {
222         @DbUnitTester
223         static IDatabaseTester databaseTester;
224 
225         @Test
226         @DbUnitPrep("annotation-it-prep.xml")
227         @DbUnitExpected(value = "annotation-it-expected.xml",
228                 verify = @DbUnitVerifyTable(value = TEST_TABLE,
229                         include = {"COLUMN0", "COLUMN1"}))
230         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
231         void mutateTheSeededRow(final Connection connection) throws Exception
232         {
233             try (Statement statement = connection.createStatement())
234             {
235                 statement.execute("UPDATE " + TEST_TABLE
236                         + " SET COLUMN1 = 'after' WHERE COLUMN0 = 'row0'");
237             }
238         }
239     }
240 
241     @ExtendWith(DbUnitExtension.class)
242     @ClearRowCountCheckSystemProperties
243     static class AutoCommitOffSetupTeardownSample
244     {
245         @DbUnitTester
246         static IDatabaseTester databaseTester;
247 
248         @Test
249         @DbUnitPrep("annotation-it-prep.xml")
250         @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
251         void seedsThePrepDataset()
252         {
253             // The @DbUnitPrep CLEAN_INSERT is the point of interest; the body does nothing.
254         }
255     }
256 }