Pruebas de integración de Kotlin con Docker: nivel avanzado
Las pruebas de integración son una etapa clave para verificar la interacción entre los componentes de una aplicación. En el ecosistema de Kotlin y Docker, este proceso alcanza un nuevo nivel gracias a la contenedorización. En este artículo, exploraremos técnicas avanzadas para configurar y ejecutar pruebas de integración utilizando Docker, Testcontainers y Kotlin DSL.
1. Fundamentos: Testcontainers con Kotlin DSL
Testcontainers es una biblioteca para Java/Kotlin que gestiona el ciclo de vida de los contenedores Docker dentro de las pruebas. Para Kotlin, está disponible un DSL conveniente que simplifica la configuración.
// build.gradle.ktsdependencies { testImplementation("org.testcontainers:testcontainers:1.20.0") testImplementation("org.testcontainers:postgresql:1.20.0") testImplementation("org.jetbrains.kotlin:kotlin-test")}Ejemplo de prueba con PostgreSQL:
import org.testcontainers.containers.PostgreSQLContainerimport org.testcontainers.junit.jupiter.Containerimport org.testcontainers.junit.jupiter.Testcontainersimport org.junit.jupiter.api.Test
@Testcontainersclass UserRepositoryTest {
companion object { @Container val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") }
@Test fun `should connect to database`() { val jdbcUrl = postgres.jdbcUrl // Usamos jdbcUrl en DataSource println("Connected to: $jdbcUrl") }}2. Ejecución paralela de contenedores
Para acelerar las pruebas, se pueden ejecutar contenedores en paralelo. Sin embargo, es importante tener en cuenta los conflictos de puertos. Utilice puertos dinámicos y sincronización a través de GenericContainer.
import org.testcontainers.containers.GenericContainerimport org.testcontainers.containers.Networkimport org.testcontainers.containers.wait.strategy.Waitimport java.time.Duration
class ParallelContainerTest {
companion object { private val network = Network.newNetwork()
private val redis1 = GenericContainer<Nothing>("redis:7-alpine") .withNetwork(network) .withNetworkAliases("redis1") .withExposedPorts(6379) .waitingFor(Wait.forListeningPort())
private val redis2 = GenericContainer<Nothing>("redis:7-alpine") .withNetwork(network) .withNetworkAliases("redis2") .withExposedPorts(6379) .waitingFor(Wait.forListeningPort())
init { redis1.start() redis2.start() } }
@Test fun `parallel containers test`() { val port1 = redis1.getMappedPort(6379) val port2 = redis2.getMappedPort(6379) println("Redis1 port: $port1, Redis2 port: $port2") }}Utilice Network para el aislamiento y waitingFor para esperar a que el contenedor esté listo.
3. Docker Compose en las pruebas
Para escenarios complejos (base de datos + broker de mensajes + caché) es conveniente usar DockerComposeContainer. Este carga el archivo docker-compose.yml y gestiona todos los servicios.
import org.testcontainers.containers.DockerComposeContainerimport java.io.File
class FullStackTest {
companion object { private val compose = DockerComposeContainer<Nothing>( File("src/test/resources/docker-compose.yml") ).apply { withExposedService("postgres_1", 5432) withExposedService("redis_1", 6379) withLocalCompose(true) start() } }
@Test fun `full integration test`() { val pgPort = compose.getServicePort("postgres_1", 5432) val redisPort = compose.getServicePort