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;
22  
23  import static org.assertj.core.api.Assertions.assertThat;
24  import static org.assertj.core.api.Assertions.assertThatCode;
25  import static org.assertj.core.api.Assertions.catchThrowable;
26  
27  import java.sql.Connection;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  import java.sql.Statement;
31  import java.util.LinkedHashMap;
32  import java.util.Map;
33  import java.util.concurrent.atomic.AtomicInteger;
34  
35  import org.dbunit.database.DatabaseConnection;
36  import org.dbunit.database.IDatabaseConnection;
37  import org.dbunit.operation.DatabaseOperation;
38  import org.dbunit.util.fileloader.DataFileLoader;
39  import org.dbunit.util.fileloader.FlatXmlDataFileLoader;
40  import org.junit.jupiter.api.AfterEach;
41  import org.junit.jupiter.api.BeforeEach;
42  import org.junit.jupiter.api.Test;
43  
44  /**
45   * Integration test of {@link MultiDataSourcePrepAndExpectedTestCase} against three independent
46   * in-memory HSQLDB databases - a {@code catalog}, an {@code orders}, and an {@code inventory}
47   * data source - each with one table, standing in for an application spanning multiple databases.
48   * Inline SQL statements stand in for the application's own service layer, writing to all three
49   * databases in one {@link PrepAndExpectedTestCaseSteps#run()} call.
50   * <p>
51   * {@link MultiDataSourcePrepAndExpectedTestCaseTest} covers this class's orchestration logic
52   * (wiring, fan-out, ordering, failure aggregation) in isolation via recording/stub delegates; this
53   * class instead proves the same wrapper end to end against real connections, real cleanup, and
54   * real assertion failures.
55   *
56   * @since 3.6.0
57   */
58  class MultiDataSourcePrepAndExpectedTestCaseIT
59  {
60      private static final String CATALOG = "catalog";
61  
62      private static final String ORDERS = "orders";
63  
64      private static final String INVENTORY = "inventory";
65  
66      private static final String CATALOG_PREP = "/xml/multiDataSourceCatalogPrep.xml";
67  
68      private static final String CATALOG_EXPECTED = "/xml/multiDataSourceCatalogExpected.xml";
69  
70      private static final String ORDERS_PREP = "/xml/multiDataSourceOrdersPrep.xml";
71  
72      private static final String ORDERS_EXPECTED = "/xml/multiDataSourceOrdersExpected.xml";
73  
74      private static final String ORDERS_EXPECTED_MISMATCH =
75              "/xml/multiDataSourceOrdersExpectedMismatch.xml";
76  
77      private static final String INVENTORY_PREP = "/xml/multiDataSourceInventoryPrep.xml";
78  
79      private static final String INVENTORY_EXPECTED = "/xml/multiDataSourceInventoryExpected.xml";
80  
81      private static final String INVENTORY_EXPECTED_MISMATCH =
82              "/xml/multiDataSourceInventoryExpectedMismatch.xml";
83  
84      private static final VerifyTableDefinition PRODUCT_TABLE =
85              new VerifyTableDefinition("PRODUCT", new String[0]);
86  
87      // ID is database-generated: exclude it from comparison and sort on only the remaining
88      // (filtered) columns, so an unpredictable insertion/identity order can't misalign otherwise
89      // equal rows
90      private static final VerifyTableDefinition PURCHASE_ORDER_TABLE = new VerifyTableDefinition(
91              "PURCHASE_ORDER", new String[] {"ID"}, null, null, true);
92  
93      private static final VerifyTableDefinition STOCK_TABLE =
94              new VerifyTableDefinition("STOCK", new String[0]);
95  
96      private static final AtomicInteger RUN_COUNTER = new AtomicInteger();
97  
98      private final DataFileLoader dataFileLoader = new FlatXmlDataFileLoader();
99  
100     private String catalogDbName;
101 
102     private String ordersDbName;
103 
104     private String inventoryDbName;
105 
106     private Connection catalogJdbcConnection;
107 
108     private Connection ordersJdbcConnection;
109 
110     private Connection inventoryJdbcConnection;
111 
112     private MultiDataSourcePrepAndExpectedTestCase testCase;
113 
114     @BeforeEach
115     void setUp() throws Exception
116     {
117         final int run = RUN_COUNTER.incrementAndGet();
118         catalogDbName = "mem:multiDsCatalog" + run;
119         ordersDbName = "mem:multiDsOrders" + run;
120         inventoryDbName = "mem:multiDsInventory" + run;
121 
122         catalogJdbcConnection = HypersonicEnvironment.createJdbcConnection(catalogDbName);
123         ordersJdbcConnection = HypersonicEnvironment.createJdbcConnection(ordersDbName);
124         inventoryJdbcConnection = HypersonicEnvironment.createJdbcConnection(inventoryDbName);
125 
126         createSchema(catalogJdbcConnection,
127                 "CREATE TABLE PRODUCT (SKU VARCHAR(20) PRIMARY KEY, NAME VARCHAR(60),"
128                         + " UNIT_PRICE DECIMAL(10,2), UNITS_SOLD INTEGER)");
129         createSchema(ordersJdbcConnection,
130                 "CREATE TABLE PURCHASE_ORDER (ID INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY"
131                         + " KEY, CUSTOMER_ID BIGINT, SKU VARCHAR(20), QUANTITY INTEGER, TOTAL"
132                         + " DECIMAL(10,2))");
133         createSchema(inventoryJdbcConnection,
134                 "CREATE TABLE STOCK (SKU VARCHAR(20) PRIMARY KEY, QTY_ON_HAND INTEGER)");
135 
136         testCase = MultiDataSourcePrepAndExpectedTestCase.forTesters(dataFileLoader)
137                 .add(CATALOG, testerFor(catalogJdbcConnection))
138                 .add(ORDERS, testerFor(ordersJdbcConnection))
139                 .add(INVENTORY, testerFor(inventoryJdbcConnection));
140     }
141 
142     @AfterEach
143     void tearDown()
144     {
145         closeQuietly(catalogJdbcConnection);
146         closeQuietly(ordersJdbcConnection);
147         closeQuietly(inventoryJdbcConnection);
148     }
149 
150     @Test
151     void testRunTest_matchingPrepAndExpectedInEveryDataSource_doesNotThrow()
152     {
153         final Map<String, PrepAndExpectedTestData> data = new LinkedHashMap<>();
154         data.put(CATALOG, catalogData(CATALOG_EXPECTED));
155         data.put(ORDERS, ordersData(ORDERS_EXPECTED));
156         data.put(INVENTORY, inventoryData(INVENTORY_EXPECTED));
157 
158         assertThatCode(() -> testCase.runTest(data, this::placeOrder))
159                 .as("Matching prep and expected data in every data source must not throw.")
160                 .doesNotThrowAnyException();
161     }
162 
163     @Test
164     void testRunTest_expectedMismatchInTwoDataSources_throwsAssertionErrorNamingBoth()
165     {
166         final Map<String, PrepAndExpectedTestData> data = new LinkedHashMap<>();
167         data.put(CATALOG, catalogData(CATALOG_EXPECTED));
168         data.put(ORDERS, ordersData(ORDERS_EXPECTED_MISMATCH));
169         data.put(INVENTORY, inventoryData(INVENTORY_EXPECTED_MISMATCH));
170 
171         final Throwable thrown = catchThrowable(() -> testCase.runTest(data, this::placeOrder));
172 
173         assertThat(thrown)
174                 .as("A mismatch in two data sources must surface as one"
175                         + " MultiDataSourceAssertionError, not stop at the first.")
176                 .isInstanceOf(MultiDataSourceAssertionError.class);
177         assertThat(thrown.getMessage())
178                 .as("The aggregate message must name both mismatched data sources.")
179                 .contains(ORDERS).contains(INVENTORY);
180     }
181 
182     @Test
183     void testRunTest_dataSourceOmittedFromTheRow_itsConnectionIsNeverOpened() throws Exception
184     {
185         testCase = MultiDataSourcePrepAndExpectedTestCase.forTesters(dataFileLoader)
186                 .add(CATALOG, testerFor(catalogJdbcConnection))
187                 .add(ORDERS, testerFor(ordersJdbcConnection))
188                 .add(INVENTORY, new ThrowingDatabaseTester());
189 
190         final Map<String, PrepAndExpectedTestData> data = new LinkedHashMap<>();
191         data.put(CATALOG, catalogData(CATALOG_EXPECTED));
192         data.put(ORDERS, ordersData(ORDERS_EXPECTED));
193         // inventory intentionally omitted: its tester throws if its connection is ever opened
194 
195         assertThatCode(() -> testCase.runTest(data, this::placeOrder))
196                 .as("A data source omitted from the data map must never have its connection"
197                         + " opened, even when opening it would throw.")
198                 .doesNotThrowAnyException();
199     }
200 
201     @Test
202     void testRunTest_success_cleansAndClosesEveryConnection() throws Exception
203     {
204         final Map<String, PrepAndExpectedTestData> data = new LinkedHashMap<>();
205         data.put(CATALOG, catalogData(CATALOG_EXPECTED));
206         data.put(ORDERS, ordersData(ORDERS_EXPECTED));
207         data.put(INVENTORY, inventoryData(INVENTORY_EXPECTED));
208 
209         testCase.runTest(data, this::placeOrder);
210 
211         assertThat(catalogJdbcConnection.isClosed())
212                 .as("catalog's connection must be closed after a successful runTest(...).")
213                 .isTrue();
214         assertThat(ordersJdbcConnection.isClosed())
215                 .as("orders' connection must be closed after a successful runTest(...).")
216                 .isTrue();
217         assertThat(inventoryJdbcConnection.isClosed())
218                 .as("inventory's connection must be closed after a successful runTest(...).")
219                 .isTrue();
220 
221         assertRowCount(catalogDbName, "PRODUCT", 0);
222         assertRowCount(ordersDbName, "PURCHASE_ORDER", 0);
223         assertRowCount(inventoryDbName, "STOCK", 0);
224     }
225 
226     /**
227      * Stands in for the application's service layer: one call writing to all three databases,
228      * via plain JDBC rather than any dbUnit type.
229      */
230     private Object placeOrder() throws Exception
231     {
232         try (Statement statement = catalogJdbcConnection.createStatement())
233         {
234             statement.executeUpdate(
235                     "UPDATE PRODUCT SET UNITS_SOLD = UNITS_SOLD + 3 WHERE SKU = 'WIDGET-1'");
236         }
237         try (Statement statement = ordersJdbcConnection.createStatement())
238         {
239             statement.executeUpdate("INSERT INTO PURCHASE_ORDER (CUSTOMER_ID, SKU, QUANTITY,"
240                     + " TOTAL) VALUES (42, 'WIDGET-1', 3, 29.97)");
241         }
242         try (Statement statement = inventoryJdbcConnection.createStatement())
243         {
244             statement.executeUpdate(
245                     "UPDATE STOCK SET QTY_ON_HAND = QTY_ON_HAND - 3 WHERE SKU = 'WIDGET-1'");
246         }
247         return null;
248     }
249 
250     private static PrepAndExpectedTestData catalogData(final String expectedFile)
251     {
252         return new PrepAndExpectedTestData(new VerifyTableDefinition[] {PRODUCT_TABLE},
253                 new String[] {CATALOG_PREP}, new String[] {expectedFile});
254     }
255 
256     private static PrepAndExpectedTestData ordersData(final String expectedFile)
257     {
258         return new PrepAndExpectedTestData(new VerifyTableDefinition[] {PURCHASE_ORDER_TABLE},
259                 new String[] {ORDERS_PREP}, new String[] {expectedFile});
260     }
261 
262     private static PrepAndExpectedTestData inventoryData(final String expectedFile)
263     {
264         return new PrepAndExpectedTestData(new VerifyTableDefinition[] {STOCK_TABLE},
265                 new String[] {INVENTORY_PREP}, new String[] {expectedFile});
266     }
267 
268     private static void createSchema(final Connection connection, final String ddl)
269             throws Exception
270     {
271         DdlExecutor.executeSql(connection, ddl);
272     }
273 
274     private static IDatabaseTester testerFor(final Connection connection) throws Exception
275     {
276         final IDatabaseConnection databaseConnection = new DatabaseConnection(connection);
277         final IDatabaseTester tester = new DefaultDatabaseTester(databaseConnection);
278         tester.setTearDownOperation(DatabaseOperation.DELETE_ALL);
279         return tester;
280     }
281 
282     private static void assertRowCount(final String dbName, final String tableName,
283             final int expectedRows) throws Exception
284     {
285         try (Connection connection = HypersonicEnvironment.createJdbcConnection(dbName);
286                 Statement statement = connection.createStatement();
287                 ResultSet resultSet =
288                         statement.executeQuery("SELECT COUNT(*) FROM " + tableName))
289         {
290             resultSet.next();
291             assertThat(resultSet.getInt(1))
292                     .as("Table " + tableName + " must be empty; a successful runTest(...) must"
293                             + " have cleaned it up.")
294                     .isEqualTo(expectedRows);
295         }
296     }
297 
298     private static void closeQuietly(final Connection connection)
299     {
300         try
301         {
302             if (connection != null && !connection.isClosed())
303             {
304                 connection.close();
305             }
306         } catch (final SQLException e)
307         {
308             // best-effort cleanup between tests; a connection cleanupData() already closed is
309             // expected to land here
310         }
311     }
312 
313     /**
314      * An {@link IDatabaseTester} that throws if its connection is ever requested, for proving a
315      * data source omitted from a {@code runTest(...)} data map is never touched.
316      */
317     private static final class ThrowingDatabaseTester extends AbstractDatabaseTester
318     {
319         @Override
320         public IDatabaseConnection getConnection()
321         {
322             throw new AssertionError("This data source's connection must never be opened when"
323                     + " it is omitted from the runTest(...) data map.");
324         }
325     }
326 }