Summary
DefaultPrepAndExpectedTestCase with setCloseConnectionAfterTest(false), reused across test methods (a base-class / @DbUnitTestCase field, paired with a CachingConnectionProvider), pins the first IDatabaseConnection it acquires in a private field and never re-checks it. If that connection's underlying JDBC connection is closed between test methods — pool max-lifetime/reap, PGConnectionPoolDataSource issuing a fresh logical handle, a bounced application context, an idle-in-transaction server kill — every following test on that instance fails, and keeps failing.
Affected versions
3.4.0, 3.5.0, 3.5.1 — setCloseConnectionAfterTest was added in 3.4.0 (#800). The 3.6.0 row count check surfaces the failure one lifecycle step earlier (preTest() rather than setupData()) but is not the cause.
Root cause
getReusableConnection() returns the cached connection field whenever it is non-null. closeReusableConnection() deliberately keeps that field set — not closing it — when closeConnectionAfterTest is false, with no check that the kept connection is still open. CachingConnectionProvider's own isClosed() / isValid() liveness check, which exists to replace a dead connection transparently, is never reached because the provider is not consulted again.
Failure shape
preTest() → captureRowCountBaseline() → RowCountChecker.capture() → QueryPerTableRowCounter.countRows() → AbstractDatabaseConnection.getRowCount() → Connection.createStatement() on the closed connection
- then, as a suppressed exception,
postTest(false) → cleanupData() → AbstractDatabaseTester.onTearDown() → DeleteAllOperation.execute() on the same closed connection
Reproduction
- One
DefaultPrepAndExpectedTestCase instance, closeConnectionAfterTest=false, backed by a DataSourceDatabaseTester + shared CachingConnectionProvider.
- Run a full
runTest(...) cycle — a connection is acquired and pinned.
- Close that connection's underlying
java.sql.Connection (simulating the pool/server dropping it).
- Run a second cycle on the same instance →
SQLException from captureRowCountBaseline(), and again from cleanupData()'s tear down operation.
Expected
The reused instance notices the connection is closed and re-acquires from the tester, letting the CachingConnectionProvider hand back a live replacement — the "transparently replaces a dead connection" guarantee the provider already documents.
Summary
DefaultPrepAndExpectedTestCasewithsetCloseConnectionAfterTest(false), reused across test methods (a base-class /@DbUnitTestCasefield, paired with aCachingConnectionProvider), pins the firstIDatabaseConnectionit acquires in a private field and never re-checks it. If that connection's underlying JDBC connection is closed between test methods — pool max-lifetime/reap,PGConnectionPoolDataSourceissuing a fresh logical handle, a bounced application context, an idle-in-transaction server kill — every following test on that instance fails, and keeps failing.Affected versions
3.4.0, 3.5.0, 3.5.1 —
setCloseConnectionAfterTestwas added in 3.4.0 (#800). The 3.6.0 row count check surfaces the failure one lifecycle step earlier (preTest()rather thansetupData()) but is not the cause.Root cause
getReusableConnection()returns the cachedconnectionfield whenever it is non-null.closeReusableConnection()deliberately keeps that field set — not closing it — whencloseConnectionAfterTestis false, with no check that the kept connection is still open.CachingConnectionProvider's ownisClosed()/isValid()liveness check, which exists to replace a dead connection transparently, is never reached because the provider is not consulted again.Failure shape
preTest()→captureRowCountBaseline()→RowCountChecker.capture()→QueryPerTableRowCounter.countRows()→AbstractDatabaseConnection.getRowCount()→Connection.createStatement()on the closed connectionpostTest(false)→cleanupData()→AbstractDatabaseTester.onTearDown()→DeleteAllOperation.execute()on the same closed connectionReproduction
DefaultPrepAndExpectedTestCaseinstance,closeConnectionAfterTest=false, backed by aDataSourceDatabaseTester+ sharedCachingConnectionProvider.runTest(...)cycle — a connection is acquired and pinned.java.sql.Connection(simulating the pool/server dropping it).SQLExceptionfromcaptureRowCountBaseline(), and again fromcleanupData()'s tear down operation.Expected
The reused instance notices the connection is closed and re-acquires from the tester, letting the
CachingConnectionProviderhand back a live replacement — the "transparently replaces a dead connection" guarantee the provider already documents.