From c8f8f2f893470c7ca74076016f0f3aae5f1b6b4b Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 3 Apr 2026 23:48:31 +0200 Subject: [PATCH] fix(tests): resolve flaky Testcontainers lifecycle causing intermittent CI failures Move @Container fields to companion objects in all @TestInstance(PER_CLASS) test classes so Testcontainers manages the container lifecycle correctly (static/shared scope instead of per-instance). Also set maxParallelForks=1 to prevent Exposed's global Database registry from being overwritten by concurrently running test classes, which caused random PSQLException: Connection refused errors on CI. --- CHANGELOG.md | 3 +++ backend/build.gradle.kts | 1 + .../shoppinglist/repository/ItemRepositoryTest.kt | 13 ++++++++----- .../shoppinglist/repository/ListRepositoryTest.kt | 13 ++++++++----- .../repository/PushSubscriptionRepositoryTest.kt | 13 ++++++++----- .../shoppinglist/repository/TransactionTest.kt | 13 ++++++++----- .../com/shoppinglist/routes/ListRoutesTest.kt | 13 ++++++++----- .../shoppinglist/service/CleanupServiceTest.kt | 15 +++++++++------ .../websocket/WebSocketIntegrationTest.kt | 13 ++++++++----- 9 files changed, 61 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bf79e2..bcf73bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **Flaky backend tests** — Resolved intermittent `PSQLException: Connection refused` errors in CI by fixing Testcontainers lifecycle mismatch (`@Container` moved to `companion object` for all `@TestInstance(PER_CLASS)` test classes) and disabling parallel test-class execution (`maxParallelForks = 1`) to prevent Exposed's global database registry from being overwritten mid-test by a concurrently running test class + ## [6.3.0] - 2026-03-30 ### Added diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 49fbe8f..0561c80 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -86,4 +86,5 @@ tasks.withType().configureEach tasks.test { useJUnitPlatform() + maxParallelForks = 1 } \ No newline at end of file diff --git a/backend/src/test/kotlin/com/shoppinglist/repository/ItemRepositoryTest.kt b/backend/src/test/kotlin/com/shoppinglist/repository/ItemRepositoryTest.kt index ea882f5..47bf436 100644 --- a/backend/src/test/kotlin/com/shoppinglist/repository/ItemRepositoryTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/repository/ItemRepositoryTest.kt @@ -23,11 +23,14 @@ import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ItemRepositoryTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_shopping_lists") - .withUsername("test_user") - .withPassword("test_pass") + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_shopping_lists") + .withUsername("test_user") + .withPassword("test_pass") + } private lateinit var database: Database private lateinit var listRepository: ListRepository diff --git a/backend/src/test/kotlin/com/shoppinglist/repository/ListRepositoryTest.kt b/backend/src/test/kotlin/com/shoppinglist/repository/ListRepositoryTest.kt index faad6c5..150183e 100644 --- a/backend/src/test/kotlin/com/shoppinglist/repository/ListRepositoryTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/repository/ListRepositoryTest.kt @@ -24,11 +24,14 @@ import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ListRepositoryTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_shopping_lists") - .withUsername("test_user") - .withPassword("test_pass") + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_shopping_lists") + .withUsername("test_user") + .withPassword("test_pass") + } private lateinit var database: Database private lateinit var listRepository: ListRepository diff --git a/backend/src/test/kotlin/com/shoppinglist/repository/PushSubscriptionRepositoryTest.kt b/backend/src/test/kotlin/com/shoppinglist/repository/PushSubscriptionRepositoryTest.kt index 5f0fd01..1328171 100644 --- a/backend/src/test/kotlin/com/shoppinglist/repository/PushSubscriptionRepositoryTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/repository/PushSubscriptionRepositoryTest.kt @@ -27,11 +27,14 @@ import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) class PushSubscriptionRepositoryTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_shopping_lists") - .withUsername("test_user") - .withPassword("test_pass") + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_shopping_lists") + .withUsername("test_user") + .withPassword("test_pass") + } private lateinit var database: Database private lateinit var listRepository: ListRepository diff --git a/backend/src/test/kotlin/com/shoppinglist/repository/TransactionTest.kt b/backend/src/test/kotlin/com/shoppinglist/repository/TransactionTest.kt index 20f80de..8360b04 100644 --- a/backend/src/test/kotlin/com/shoppinglist/repository/TransactionTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/repository/TransactionTest.kt @@ -27,11 +27,14 @@ import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TransactionTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_shopping_lists") - .withUsername("test_user") - .withPassword("test_pass") + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_shopping_lists") + .withUsername("test_user") + .withPassword("test_pass") + } private lateinit var database: Database private lateinit var listRepository: ListRepository diff --git a/backend/src/test/kotlin/com/shoppinglist/routes/ListRoutesTest.kt b/backend/src/test/kotlin/com/shoppinglist/routes/ListRoutesTest.kt index 3547251..60e6b29 100644 --- a/backend/src/test/kotlin/com/shoppinglist/routes/ListRoutesTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/routes/ListRoutesTest.kt @@ -28,11 +28,14 @@ import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ListRoutesTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_shopping_lists") - .withUsername("test_user") - .withPassword("test_pass") + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_shopping_lists") + .withUsername("test_user") + .withPassword("test_pass") + } private lateinit var database: Database private val json = Json { ignoreUnknownKeys = true } diff --git a/backend/src/test/kotlin/com/shoppinglist/service/CleanupServiceTest.kt b/backend/src/test/kotlin/com/shoppinglist/service/CleanupServiceTest.kt index 3136a15..5c1417c 100644 --- a/backend/src/test/kotlin/com/shoppinglist/service/CleanupServiceTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/service/CleanupServiceTest.kt @@ -27,12 +27,15 @@ import kotlin.test.assertNull @TestInstance(TestInstance.Lifecycle.PER_CLASS) class CleanupServiceTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_cleanup") - .withUsername("test_user") - .withPassword("test_pass") - + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_cleanup") + .withUsername("test_user") + .withPassword("test_pass") + } + private lateinit var database: Database private lateinit var cleanupService: CleanupService private lateinit var listRepository: ListRepositoryImpl diff --git a/backend/src/test/kotlin/com/shoppinglist/websocket/WebSocketIntegrationTest.kt b/backend/src/test/kotlin/com/shoppinglist/websocket/WebSocketIntegrationTest.kt index bbadff0..b0f5fc7 100644 --- a/backend/src/test/kotlin/com/shoppinglist/websocket/WebSocketIntegrationTest.kt +++ b/backend/src/test/kotlin/com/shoppinglist/websocket/WebSocketIntegrationTest.kt @@ -30,11 +30,14 @@ import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WebSocketIntegrationTest { - @Container - private val postgres = PostgreSQLContainer("postgres:15-alpine") - .withDatabaseName("test_shopping_lists") - .withUsername("test_user") - .withPassword("test_pass") + companion object { + @Container + @JvmField + val postgres = PostgreSQLContainer("postgres:15-alpine") + .withDatabaseName("test_shopping_lists") + .withUsername("test_user") + .withPassword("test_pass") + } private lateinit var database: Database private val json = Json { ignoreUnknownKeys = true }