chore: run CI test in docker
Tests / build (push) Successful in 7m13s
Tests / test (push) Failing after 9m19s
Tests / lint (push) Successful in 13m33s

This commit is contained in:
2026-08-04 20:28:49 +02:00
parent e262f35b27
commit b60e4aa457
10 changed files with 100 additions and 90 deletions
+22 -18
View File
@@ -80,36 +80,40 @@ jobs:
continue-on-error: false continue-on-error: false
test: test:
needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
GRADLE_CACHE_DIR: ${{ github.workspace }}/.gradle-docker-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up JDK 21 - name: Prepare docker secrets
uses: actions/setup-java@v5 run: |
with: [ -f docker/postgresql.secret ] || echo -n "changeit" > docker/postgresql.secret
distribution: 'temurin'
java-version: '21'
- 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 uses: actions/cache@v6
with: with:
path: | path: ${{ env.GRADLE_CACHE_DIR }}
~/.gradle/caches key: ${{ steps.cache-key-generator.outputs.key }}
~/.gradle/wrapper
key: ${{ needs.build.outputs.cache-key }}
restore-keys: | restore-keys: |
gradle-${{ runner.os }}- gradle-docker-${{ runner.os }}-
- name: Grant execute permission to Gradle wrapper - name: Prepare cache directory permissions
run: chmod +x gradlew run: |
mkdir -p "$GRADLE_CACHE_DIR"
chmod -R 777 "$GRADLE_CACHE_DIR"
- name: Start CI Docker Compose services - name: Run tests in Docker
run: ./gradlew ciComposeUp -Pci run: docker compose -f docker/docker-compose-test.yaml run --rm tests
- name: Run tests - name: Shut down Docker services
run: ./gradlew test -x ciComposeUp -Pci --no-daemon if: always()
run: docker compose -f docker/docker-compose-test.yaml down -v
- name: Upload test reports - name: Upload test reports
if: always() if: always()
+1
View File
@@ -37,3 +37,4 @@ out/
/docker/.env /docker/.env
/docker/*.secret /docker/*.secret
*.hprof *.hprof
/.gradle-docker-cache/
+11 -37
View File
@@ -1,4 +1,3 @@
import com.avast.gradle.dockercompose.ComposeExtension
import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.KtlintExtension
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version") val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
@@ -15,7 +14,6 @@ plugins {
id("io.ktor.plugin") version "3.5.1" id("io.ktor.plugin") version "3.5.1"
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10" id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10"
id("org.jlleitschuh.gradle.ktlint") version "14.2.0" id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
id("com.avast.gradle.docker-compose") version "0.17.12"
} }
group = "io.github.flecomte" group = "io.github.flecomte"
@@ -48,43 +46,19 @@ java {
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
useJUnitPlatform() useJUnitPlatform()
} jvmArgs("-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading")
// Dynamic self-attach (used by MockK/ByteBuddy) times out in Docker containers because the
configure<ComposeExtension> { // SIGQUIT-triggered AttachListener handshake never completes there. Loading the byte-buddy
createNested("test").apply { // agent jar statically via -javaagent avoids the attach handshake entirely: MockK detects the
useComposeFiles.set(listOf("docker/docker-compose-test.yaml")) // pre-installed Instrumentation instance and skips dynamic attach.
setProjectName("event-demo-test") doFirst {
} val agentJar =
createNested("dev").apply { classpath.files.firstOrNull { it.name.startsWith("byte-buddy-agent") }
useComposeFiles.set(listOf("docker/docker-compose-dev.yaml")) ?: error("byte-buddy-agent jar not found on test classpath")
setProjectName("event-demo-dev") jvmArgs("-javaagent:$agentJar")
}
createNested("ci").apply {
useComposeFiles.set(listOf("docker/docker-compose-ci.yaml"))
setProjectName("event-demo-ci")
} }
} }
tasks.test { 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") dependsOn("copyEnv")
} }
@@ -154,6 +128,6 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}") testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.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") testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
} }
+10
View File
@@ -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"]
+5 -1
View File
@@ -1,4 +1,8 @@
name: event-demo-test name: event-demo-test
include: include:
- path: - path:
- docker-compose-test.yaml - 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
+1 -3
View File
@@ -2,7 +2,5 @@ name: event-demo-test
include: include:
- path: - path:
- parts/docker-compose-databases.yaml - parts/docker-compose-databases.yaml
- parts/docker-compose-databases-expose.yaml - parts/docker-compose-test.yaml
- parts/docker-compose-tools.yaml
- parts/docker-compose-tools-local.yaml
- parts/docker-compose-traefik.yaml - parts/docker-compose-traefik.yaml
+20
View File
@@ -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:
+3 -3
View File
@@ -13,12 +13,12 @@ jwt {
} }
redis { redis {
url = "redis://localhost:6379" url = "redis://redis:6379"
url = ${?REDIS_URL} url = ${?REDIS_URL}
} }
postgresql { postgresql {
url = "jdbc:postgresql://localhost:5432/event-demo" url = "jdbc:postgresql://postgresql/event-demo"
url = ${?POSTGRESQL_URL} url = ${?POSTGRESQL_URL}
username = "event-demo" username = "event-demo"
@@ -29,7 +29,7 @@ postgresql {
} }
rabbitmq { rabbitmq {
url = "localhost" url = "rabbitmq"
url = ${?RABBITMQ_URL} url = ${?RABBITMQ_URL}
port = "5672" port = "5672"
+21 -25
View File
@@ -2,6 +2,7 @@ package eventDemo.libs.bus
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import eventDemo.testHelpers.spyPing import eventDemo.testHelpers.spyPing
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData import io.kotest.datatest.withData
import io.kotest.matchers.string.shouldStartWith import io.kotest.matchers.string.shouldStartWith
@@ -15,33 +16,28 @@ private data class ObjTest(
class BusTest : class BusTest :
FunSpec({ FunSpec({
context("Pub/sub") { context("Pub/sub") {
val factory = testKoinApplicationWithConfig {
ConnectionFactory().apply { val list: Map<String, Bus<ObjTest>> =
host = "localhost" mapOf(
port = 5672 BusInMemory::class.java.simpleName to BusInMemory(),
username = "event-demo" BusInRabbitMQ::class.java.simpleName to
password = "changeit" BusInRabbitMQ(
} get<ConnectionFactory>(),
val list: Map<String, Bus<ObjTest>> = "testExchange",
mapOf( { it.value },
BusInMemory::class.java.simpleName to BusInMemory(), { ObjTest(it) },
BusInRabbitMQ::class.java.simpleName to ),
BusInRabbitMQ( )
factory,
"testExchange",
{ it.value },
{ ObjTest(it) },
),
)
withData(list) { bus -> withData(list) { bus ->
spyPing(exactly = 2, duration = 1.seconds) { ping -> spyPing(exactly = 2, duration = 1.seconds) { ping ->
bus.subscribe { obj -> bus.subscribe { obj ->
ping() ping()
obj.value shouldStartWith "testMessage" 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()}"))
} }
} }
} }
@@ -15,10 +15,13 @@ import org.koin.core.module.KoinApplicationDslMarker
import org.koin.dsl.koinApplication import org.koin.dsl.koinApplication
import org.koin.ktor.ext.getKoin import org.koin.ktor.ext.getKoin
const val CONFIG_FILE_NAME = "application.conf"
@KoinApplicationDslMarker @KoinApplicationDslMarker
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T = suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration)) } koinApplication {
.koin modules(appKoinModule(ApplicationConfig(CONFIG_FILE_NAME).configuration))
}.koin
.run { .run {
cleanDataTest() cleanDataTest()
configureProjectionListener() configureProjectionListener()
@@ -34,7 +37,7 @@ fun testApplicationWithConfig(
) { ) {
val logger = KotlinLogging.logger {} val logger = KotlinLogging.logger {}
testApplication { testApplication {
val conf = ApplicationConfig("application.conf") val conf = ApplicationConfig(CONFIG_FILE_NAME)
environment { environment {
config = conf config = conf
} }