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.Statement;
27  
28  import org.dbunit.DatabaseEnvironment;
29  import org.dbunit.DefaultDatabaseTester;
30  import org.dbunit.IDatabaseTester;
31  import org.dbunit.IOperationListener;
32  import org.dbunit.annotation.DbUnitRowCountCheck;
33  import org.dbunit.annotation.DbUnitSetup;
34  import org.dbunit.annotation.DbUnitTester;
35  import org.dbunit.database.DatabaseConfig;
36  import org.dbunit.database.IDatabaseConnection;
37  import org.dbunit.database.rowcount.ClearRowCountCheckSystemProperties;
38  import org.dbunit.database.rowcount.UnexpectedRowCountException;
39  import org.dbunit.operation.DbUnitOperation;
40  import org.junit.jupiter.api.Test;
41  import org.junit.jupiter.api.extension.ExtendWith;
42  import org.junit.platform.engine.TestExecutionResult;
43  import org.junit.platform.testkit.engine.EngineTestKit;
44  import org.junit.platform.testkit.engine.Event;
45  
46  /**
47   * Real-database integration test of {@link DbUnitRowCountCheck}: a leaked row in a table the
48   * test never lists, and the exclude list silencing a legitimate case of that. Both fixture
49   * classes explicitly disable {@link DatabaseConfig#FEATURE_ROW_COUNT_CHECK} on the shared
50   * connection first, so a passing run proves the annotation - not the feature - is what
51   * enables the check.
52   */
53  @ClearRowCountCheckSystemProperties
54  class DbUnitExtensionRowCountCheckIT
55  {
56      private static final String EMPTY_TABLE = "EMPTY_TABLE";
57  
58      @Test
59      void testAfterTestExecution_rowLeakedIntoUnlistedTable_failsNamingIt() throws Exception
60      {
61          final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
62          try
63          {
64              final IDatabaseConnection connection = connectionWithFeatureDisabled(environment);
65              LeakedRowSample.databaseTester = new DefaultDatabaseTester(connection);
66              LeakedRowSample.databaseTester
67                      .setOperationListener(IOperationListener.NO_OP_OPERATION_LISTENER);
68  
69              final Event failedEvent = EngineTestKit.engine("junit-jupiter")
70                      .selectors(selectClass(LeakedRowSample.class)).execute().testEvents()
71                      .failed().stream().findFirst().orElseThrow(
72                              () -> new AssertionError("Expected one failed test event."));
73  
74              final Throwable reported = failedEvent
75                      .getRequiredPayload(TestExecutionResult.class).getThrowable()
76                      .orElseThrow(() -> new AssertionError("Expected a reported throwable."));
77              assertThat(reported)
78                      .as("A row left behind in a table absent from teardown must fail the"
79                              + " test with UnexpectedRowCountException.")
80                      .isInstanceOf(UnexpectedRowCountException.class);
81              assertThat(((UnexpectedRowCountException) reported).getDifferences())
82                      .as("The failure must name the table the row leaked into; table name"
83                              + " casing is database-dependent (PostgreSQL folds it to"
84                              + " lowercase), so match case-insensitively.")
85                      .anyMatch(difference -> difference.getTableName()
86                              .equalsIgnoreCase(EMPTY_TABLE));
87          } finally
88          {
89              deleteAllRowsQuietly(environment, EMPTY_TABLE);
90              environment.closeConnection();
91          }
92      }
93  
94      @Test
95      void testAfterTestExecution_excludedTableLeaksRows_passes() throws Exception
96      {
97          final DatabaseEnvironment environment = DatabaseEnvironment.getInstance();
98          try
99          {
100             final IDatabaseConnection connection = connectionWithFeatureDisabled(environment);
101             ExcludedTableSample.databaseTester = new DefaultDatabaseTester(connection);
102             ExcludedTableSample.databaseTester
103                     .setOperationListener(IOperationListener.NO_OP_OPERATION_LISTENER);
104 
105             EngineTestKit.engine("junit-jupiter")
106                     .selectors(selectClass(ExcludedTableSample.class)).execute().testEvents()
107                     .assertStatistics(stats -> stats.started(1).succeeded(1));
108         } finally
109         {
110             deleteAllRowsQuietly(environment, EMPTY_TABLE);
111             environment.closeConnection();
112         }
113     }
114 
115     private static IDatabaseConnection connectionWithFeatureDisabled(
116             final DatabaseEnvironment environment) throws Exception
117     {
118         final IDatabaseConnection connection = environment.getConnection();
119         connection.getConfig().setFeature(DatabaseConfig.FEATURE_ROW_COUNT_CHECK, false);
120         return connection;
121     }
122 
123     private static void deleteAllRowsQuietly(final DatabaseEnvironment environment,
124             final String tableName)
125     {
126         try (Statement statement =
127                 environment.getConnection().getConnection().createStatement())
128         {
129             statement.execute("DELETE FROM " + tableName);
130         } catch (final Exception e)
131         {
132             // best-effort cleanup only; a failure here must not fail the test that already ran
133         }
134     }
135 
136     @ExtendWith(DbUnitExtension.class)
137     @DbUnitRowCountCheck
138     @DbUnitSetup(operation = DbUnitOperation.NONE)
139     static class LeakedRowSample
140     {
141         @DbUnitTester
142         static IDatabaseTester databaseTester;
143 
144         @Test
145         void testInsertRow_intoUnlistedTable_leavesRowForRowCountCheckToDetect() throws Exception
146         {
147             try (Statement statement =
148                     databaseTester.getConnection().getConnection().createStatement())
149             {
150                 statement.execute(
151                         "INSERT INTO " + EMPTY_TABLE + " (COLUMN0) VALUES ('leaked')");
152             }
153         }
154     }
155 
156     @ExtendWith(DbUnitExtension.class)
157     @DbUnitRowCountCheck(exclude = EMPTY_TABLE)
158     @DbUnitSetup(operation = DbUnitOperation.NONE)
159     static class ExcludedTableSample
160     {
161         @DbUnitTester
162         static IDatabaseTester databaseTester;
163 
164         @Test
165         void testInsertRow_intoExcludedTable_rowCountCheckIgnoresIt() throws Exception
166         {
167             try (Statement statement =
168                     databaseTester.getConnection().getConnection().createStatement())
169             {
170                 statement.execute(
171                         "INSERT INTO " + EMPTY_TABLE + " (COLUMN0) VALUES ('leaked')");
172             }
173         }
174     }
175 }