From b60e4aa457a552c5db37728713fc585c95f17b2b Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Mon, 3 Aug 2026 21:56:52 +0200 Subject: [PATCH] chore: run CI test in docker --- .github/workflows/tests.yml | 40 +++++++++------- .gitignore | 1 + build.gradle.kts | 48 +++++-------------- docker/DockerfileTest | 10 ++++ docker/docker-compose-dev.yaml | 6 ++- docker/docker-compose-test.yaml | 4 +- docker/parts/docker-compose-test.yaml | 20 ++++++++ src/main/resources/application.conf | 6 +-- src/test/kotlin/eventDemo/libs/bus/BusTest.kt | 46 ++++++++---------- .../testHelpers/TestApplicationHelpers.kt | 9 ++-- 10 files changed, 100 insertions(+), 90 deletions(-) create mode 100644 docker/DockerfileTest create mode 100644 docker/parts/docker-compose-test.yaml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31f5828..65f7ba9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,36 +80,40 @@ jobs: continue-on-error: false test: - needs: build runs-on: ubuntu-latest + env: + GRADLE_CACHE_DIR: ${{ github.workspace }}/.gradle-docker-cache steps: - name: Checkout code uses: actions/checkout@v6 - - name: Set up JDK 21 - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '21' + - name: Prepare docker secrets + run: | + [ -f docker/postgresql.secret ] || echo -n "changeit" > docker/postgresql.secret - - name: Restore Gradle cache + - name: Generate cache key + id: cache-key-generator + run: echo "key=gradle-docker-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT + + - name: Restore Gradle cache (Docker) uses: actions/cache@v6 with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ needs.build.outputs.cache-key }} + path: ${{ env.GRADLE_CACHE_DIR }} + key: ${{ steps.cache-key-generator.outputs.key }} restore-keys: | - gradle-${{ runner.os }}- + gradle-docker-${{ runner.os }}- - - name: Grant execute permission to Gradle wrapper - run: chmod +x gradlew + - name: Prepare cache directory permissions + run: | + mkdir -p "$GRADLE_CACHE_DIR" + chmod -R 777 "$GRADLE_CACHE_DIR" - - name: Start CI Docker Compose services - run: ./gradlew ciComposeUp -Pci + - name: Run tests in Docker + run: docker compose -f docker/docker-compose-test.yaml run --rm tests - - name: Run tests - run: ./gradlew test -x ciComposeUp -Pci --no-daemon + - name: Shut down Docker services + if: always() + run: docker compose -f docker/docker-compose-test.yaml down -v - name: Upload test reports if: always() diff --git a/.gitignore b/.gitignore index 2e0d166..65cfb0e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ out/ /docker/.env /docker/*.secret *.hprof +/.gradle-docker-cache/ diff --git a/build.gradle.kts b/build.gradle.kts index 6dd53d6..8e02f94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -import com.avast.gradle.dockercompose.ComposeExtension import org.jlleitschuh.gradle.ktlint.KtlintExtension val ktorVersion: Provider = providers.gradleProperty("ktor_version") @@ -15,7 +14,6 @@ plugins { id("io.ktor.plugin") version "3.5.1" id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10" id("org.jlleitschuh.gradle.ktlint") version "14.2.0" - id("com.avast.gradle.docker-compose") version "0.17.12" } group = "io.github.flecomte" @@ -48,43 +46,19 @@ java { tasks.withType().configureEach { useJUnitPlatform() -} - -configure { - createNested("test").apply { - useComposeFiles.set(listOf("docker/docker-compose-test.yaml")) - setProjectName("event-demo-test") - } - createNested("dev").apply { - useComposeFiles.set(listOf("docker/docker-compose-dev.yaml")) - setProjectName("event-demo-dev") - } - createNested("ci").apply { - useComposeFiles.set(listOf("docker/docker-compose-ci.yaml")) - setProjectName("event-demo-ci") + jvmArgs("-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading") + // Dynamic self-attach (used by MockK/ByteBuddy) times out in Docker containers because the + // SIGQUIT-triggered AttachListener handshake never completes there. Loading the byte-buddy + // agent jar statically via -javaagent avoids the attach handshake entirely: MockK detects the + // pre-installed Instrumentation instance and skips dynamic attach. + doFirst { + val agentJar = + classpath.files.firstOrNull { it.name.startsWith("byte-buddy-agent") } + ?: error("byte-buddy-agent jar not found on test classpath") + jvmArgs("-javaagent:$agentJar") } } - tasks.test { - if (project.hasProperty("ci")) { - dependsOn("ciComposeUp") - } else { - dependsOn("testComposeUp") - } -} -tasks.named("run") { - dependsOn("devComposeUp") -} -tasks.composeUp { - dependsOn("copyEnv") -} -tasks.named("devComposeUp") { - dependsOn("copyEnv") -} -tasks.named("testComposeUp") { - dependsOn("copyEnv") -} -tasks.named("ciComposeUp") { dependsOn("copyEnv") } @@ -154,6 +128,6 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}") testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}") testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}") - testImplementation("io.mockk:mockk:1.13.17") + testImplementation("io.mockk:mockk:1.14.11") testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0") } diff --git a/docker/DockerfileTest b/docker/DockerfileTest new file mode 100644 index 0000000..183ddcd --- /dev/null +++ b/docker/DockerfileTest @@ -0,0 +1,10 @@ +# Image officielle Gradle avec JDK 21 déjà installé +FROM gradle:9.6.1-jdk21 + +WORKDIR /app + +# Copie du wrapper et des fichiers de config en premier pour profiter du cache Docker +COPY build.gradle.kts settings.gradle.kts ./ + +# Lance les tests Kotlin +CMD ["gradle", "test", "--no-daemon"] \ No newline at end of file diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml index 4bf8ca4..87b6cbf 100644 --- a/docker/docker-compose-dev.yaml +++ b/docker/docker-compose-dev.yaml @@ -1,4 +1,8 @@ name: event-demo-test include: - path: - - docker-compose-test.yaml \ No newline at end of file + - parts/docker-compose-databases.yaml + - parts/docker-compose-databases-expose.yaml + - parts/docker-compose-tools.yaml + - parts/docker-compose-tools-local.yaml + - parts/docker-compose-traefik.yaml \ No newline at end of file diff --git a/docker/docker-compose-test.yaml b/docker/docker-compose-test.yaml index f9941ef..c338ab7 100644 --- a/docker/docker-compose-test.yaml +++ b/docker/docker-compose-test.yaml @@ -2,7 +2,5 @@ name: event-demo-test include: - path: - parts/docker-compose-databases.yaml - - parts/docker-compose-databases-expose.yaml - - parts/docker-compose-tools.yaml - - parts/docker-compose-tools-local.yaml + - parts/docker-compose-test.yaml - parts/docker-compose-traefik.yaml \ No newline at end of file diff --git a/docker/parts/docker-compose-test.yaml b/docker/parts/docker-compose-test.yaml new file mode 100644 index 0000000..0e5d60b --- /dev/null +++ b/docker/parts/docker-compose-test.yaml @@ -0,0 +1,20 @@ +services: + tests: + build: + context: ../.. + dockerfile: docker/DockerfileTest + volumes: + - ${GRADLE_CACHE_DIR:-gradle-cache}:/home/gradle/.gradle + - ../..:/app + depends_on: + flyway: + condition: service_completed_successfully + postgresql: + condition: service_healthy + rabbitmq: + condition: service_healthy + redis: + condition: service_healthy + +volumes: + gradle-cache: \ No newline at end of file diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 66a0f43..0280eae 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -13,12 +13,12 @@ jwt { } redis { - url = "redis://localhost:6379" + url = "redis://redis:6379" url = ${?REDIS_URL} } postgresql { - url = "jdbc:postgresql://localhost:5432/event-demo" + url = "jdbc:postgresql://postgresql/event-demo" url = ${?POSTGRESQL_URL} username = "event-demo" @@ -29,7 +29,7 @@ postgresql { } rabbitmq { - url = "localhost" + url = "rabbitmq" url = ${?RABBITMQ_URL} port = "5672" diff --git a/src/test/kotlin/eventDemo/libs/bus/BusTest.kt b/src/test/kotlin/eventDemo/libs/bus/BusTest.kt index 9fbc168..ce61884 100644 --- a/src/test/kotlin/eventDemo/libs/bus/BusTest.kt +++ b/src/test/kotlin/eventDemo/libs/bus/BusTest.kt @@ -2,6 +2,7 @@ package eventDemo.libs.bus import com.rabbitmq.client.ConnectionFactory import eventDemo.testHelpers.spyPing +import eventDemo.testHelpers.testKoinApplicationWithConfig import io.kotest.core.spec.style.FunSpec import io.kotest.datatest.withData import io.kotest.matchers.string.shouldStartWith @@ -15,33 +16,28 @@ private data class ObjTest( class BusTest : FunSpec({ context("Pub/sub") { - val factory = - ConnectionFactory().apply { - host = "localhost" - port = 5672 - username = "event-demo" - password = "changeit" - } - val list: Map> = - mapOf( - BusInMemory::class.java.simpleName to BusInMemory(), - BusInRabbitMQ::class.java.simpleName to - BusInRabbitMQ( - factory, - "testExchange", - { it.value }, - { ObjTest(it) }, - ), - ) + testKoinApplicationWithConfig { + val list: Map> = + mapOf( + BusInMemory::class.java.simpleName to BusInMemory(), + BusInRabbitMQ::class.java.simpleName to + BusInRabbitMQ( + get(), + "testExchange", + { it.value }, + { ObjTest(it) }, + ), + ) - withData(list) { bus -> - spyPing(exactly = 2, duration = 1.seconds) { ping -> - bus.subscribe { obj -> - ping() - obj.value shouldStartWith "testMessage" + withData(list) { bus -> + spyPing(exactly = 2, duration = 1.seconds) { ping -> + bus.subscribe { obj -> + ping() + obj.value shouldStartWith "testMessage" + } + bus.publish(ObjTest("testMessage${Random.nextInt()}")) + bus.publish(ObjTest("testMessage${Random.nextInt()}")) } - bus.publish(ObjTest("testMessage${Random.nextInt()}")) - bus.publish(ObjTest("testMessage${Random.nextInt()}")) } } } diff --git a/src/test/kotlin/eventDemo/testHelpers/TestApplicationHelpers.kt b/src/test/kotlin/eventDemo/testHelpers/TestApplicationHelpers.kt index 74a4af1..d4bc83c 100644 --- a/src/test/kotlin/eventDemo/testHelpers/TestApplicationHelpers.kt +++ b/src/test/kotlin/eventDemo/testHelpers/TestApplicationHelpers.kt @@ -15,10 +15,13 @@ import org.koin.core.module.KoinApplicationDslMarker import org.koin.dsl.koinApplication import org.koin.ktor.ext.getKoin +const val CONFIG_FILE_NAME = "application.conf" + @KoinApplicationDslMarker suspend fun testKoinApplicationWithConfig(block: suspend Koin.() -> T): T = - koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration)) } - .koin + koinApplication { + modules(appKoinModule(ApplicationConfig(CONFIG_FILE_NAME).configuration)) + }.koin .run { cleanDataTest() configureProjectionListener() @@ -34,7 +37,7 @@ fun testApplicationWithConfig( ) { val logger = KotlinLogging.logger {} testApplication { - val conf = ApplicationConfig("application.conf") + val conf = ApplicationConfig(CONFIG_FILE_NAME) environment { config = conf }