Ir al contenido principal
Versión: 5.5.x

Testcontainers

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Testcontainers

nota

Esta documentación corresponde a la última versión del módulo Testcontainers y es compatible con Kotest 5.0+. Para versiones anteriores, consulta la documentación aquí

El proyecto Testcontainers proporciona instancias ligeras y efímeras de bases de datos comunes, elasticsearch, kafka, navegadores web Selenium, o cualquier otro componente que pueda ejecutarse en un contenedor Docker - ideal para usar en pruebas.

Kotest ofrece integración con Testcontainers a través de un módulo adicional que proporciona varias extensiones: especializadas para bases de datos y Kafka, y soporte genérico para cualquier imagen Docker compatible.

Dependencias

Para comenzar, añade la siguiente dependencia en tu archivo de compilación Gradle.

io.kotest.extensions:kotest-extensions-testcontainers:${kotest.version}

Nota: El group id es diferente (io.kotest.extensions) al de las dependencias principales de kotest (io.kotest).

Para Maven, necesitarás estas dependencias:

<dependency>
<groupId>io.kotest.extensions</groupId>
<artifactId>kotest-extensions-testcontainers</artifactId>
<version>${kotest.version}</version>
<scope>test</scope>
</dependency>

Bases de datos

Para bases de datos compatibles con JDBC, Kotest proporciona la extensión JdbcTestContainerExtension. Esta ofrece un javax.sql.DataSource agrupado, respaldado por una instancia de HikariCP, que puede configurarse durante la inicialización.

Primero, crea el contenedor.

val mysql = MySQLContainer<Nothing>("mysql:8.0.26").apply {
startupAttempts = 1
withUrlParam("connectionTimeZone", "Z")
withUrlParam("zeroDateTimeBehavior", "convertToNull")
}

Segundo, instala el contenedor dentro de un envoltorio de extensión, proporcionando opcionalmente una lambda de configuración.

val ds = install(JdbcTestContainerExtension(mysql)) {
poolName = "myconnectionpool"
maximumPoolSize = 8
idleTimeout = 10000
}

Si no deseas configurar el pool, puedes omitir la lambda final.

Luego el datasource puede usarse en una prueba. Por ejemplo, aquí tienes un ejemplo completo de inserción de objetos y su posterior recuperación para verificar que la inserción fue exitosa.

class QueryDatastoreTest : FunSpec({

val mysql = MySQLContainer<Nothing>("mysql:8.0.26").apply {
startupAttempts = 1
withUrlParam("connectionTimeZone", "Z")
withUrlParam("zeroDateTimeBehavior", "convertToNull")
}

val ds = install(JdbcTestContainerExtension(mysql)) {
poolName = "myconnectionpool"
maximumPoolSize = 8
idleTimeout = 10000
}

val datastore = PersonDatastore(ds)

test("insert happy path") {

datastore.insert(Person("sam", "Chicago"))
datastore.insert(Person("jim", "Seattle"))

datastore.findAll().shouldBe(listOf(
Person("sam", "Chicago"),
Person("jim", "Seattle"),
))
}
})
consejo

Esta extensión también admite la bandera LifecycleMode para controlar cuándo se inicia y se detiene el contenedor. Consulta Lifecycle

Inicialización del contenedor de base de datos

Existen dos formas de inicializar el contenedor de base de datos: mediante un script de inicialización único añadido a la configuración de TestContainer, o mediante una lista de scripts añadidos a la lambda de configuración de JdbcTestContainerExtension.

Si añades un script único mediante la configuración de TestContainer, simplemente agrega el script a la opción withInitScript de TestContainer, así:

val mysql = MySQLContainer<Nothing>("mysql:8.0.26").apply {
withInitScript("init.sql")
startupAttempts = 1
withUrlParam("connectionTimeZone", "Z")
withUrlParam("zeroDateTimeBehavior", "convertToNull")
}

Si tienes múltiples scripts de inicialización o conjuntos de cambios, puedes añadirlos como lista en la lambda de configuración dbInitScripts de la extensión, así:

val ds = install(JdbcTestContainerExtension(mysql, LifecycleMode.Leaf)) {
maximumPoolSize = 8
minimumIdle = 4
dbInitScripts = listOf("/init.sql", "/sql-changesets")
}

La lista puede contener rutas absolutas o relativas, para archivos y carpetas en el sistema de archivos o en el classpath.

La extensión procesará la lista proporcionada en orden. Si el elemento de la lista es una carpeta, procesará todos los scripts .sql en dicha carpeta, ordenados lexicográficamente. Estos scripts se ejecutan cada vez que se inicia el contenedor, por lo que soporta la bandera LifecycleMode.

Contenedores generales

Similar a JdbcTestContainerExtension, este módulo también proporciona una extensión TestContainerExtension que puede envolver cualquier contenedor, no solo bases de datos.

Podemos crear la extensión usando tanto un nombre de imagen Docker como un contenedor fuertemente tipado.

Por ejemplo, usando directamente una imagen de Docker:

val container = install(TestContainerExtension("redis:5.0.3-alpine")) {
startupAttempts = 1
withExposedPorts(6379)
}

Y luego usando un contenedor fuertemente tipado:

val elasticsearch = install(TestContainerExtension(ElasticsearchContainer(ELASTICSEARCH_IMAGE) )) {
withPassword(ELASTICSEARCH_PASSWORD)
}

Se prefiere el contenedor fuertemente tipado cuando está disponible en el proyecto Testcontainers, ya que nos da acceso a configuraciones específicas, como la opción de contraseña en el ejemplo de elasticsearch anterior.

Sin embargo, cuando no hay disponible un contenedor fuertemente tipado, el primer método nos permite iniciar cualquier imagen de Docker como contenedor general.

consejo

Esta extensión también admite la bandera LifecycleMode para controlar cuándo se inicia y se detiene el contenedor. Consulta Lifecycle

Contenedores de Kafka

Para Kafka, este módulo proporciona métodos de extensión convenientes para crear consumidores, productores o clientes de administración desde el contenedor.

val kafka = install(TestContainerExtension(KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")))) {
withEmbeddedZookeeper()
}

Dentro de la lambda de configuración, podemos especificar opciones para el contenedor de Kafka, como zookeeper integrado/externo o propiedades del broker Kafka mediante variables de entorno. Por ejemplo, para habilitar la creación dinámica de temas:

val kafka = install(TestContainerExtension(KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")))) {
withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true")
}
[Nota para usuarios de Apple Silicon/ARM]

Kafka solo publica una versión linux/amd64 del contenedor. Si estás en un equipo con arquitectura Apple Silicon/ARM, deberás especificar explícitamente la plataforma añadiendo lo siguiente a la lambda de configuración descrita anteriormente:

withCreateContainerCmdModifier { it.withPlatform("linux/amd64") }

Una vez instalado el contenedor, podemos crear un cliente usando los siguientes métodos:

  • container.createProducer()

  • container.createStringStringProducer()

  • container.createConsumer()

  • container.createStringStringConsumer()

  • container.createAdminClient()

Cada uno de estos acepta una lambda de configuración opcional para establecer valores en el objeto de propiedades que se utiliza para crear los clientes.

Por ejemplo, en esta prueba producimos y consumimos un mensaje del mismo topic, usando la lambda de configuración para establecer max.poll.records en 1.

class KafkaTestContainerExtensionTest : FunSpec() {
init {

val kafka = install(TestContainerExtension(KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")))) {
withEmbeddedZookeeper()
}

test("should send/receive message") {

val producer = kafka.createStringStringProducer()
producer.send(ProducerRecord("foo", null, "bubble bobble"))
producer.close()

val consumer = kafka.createStringStringConsumer {
this[ConsumerConfig.MAX_POLL_RECORDS_CONFIG] = 1
}

consumer.subscribe(listOf("foo"))
val records = consumer.poll(Duration.ofSeconds(100))
records.shouldHaveSize(1)
}
}
}
nota

Al crear un consumidor, el grupo de consumidores se establece como un UUID aleatorio. Para cambiarlo, proporciona una lambda de configuración y especifica tu propio group.id.

Ciclo de vida

Por defecto, el ciclo de vida de un contenedor es por especificación (spec): se inicia en el comando install y se detiene al completarse la spec. Esto puede cambiarse para iniciar/detener por test, por test hoja o por test raíz.

Para hacerlo, pasa un parámetro LifecycleMode a TestContainerExtension o JdbcTestContainerExtension.

Por ejemplo:

val ds = install(JdbcTestContainerExtension(mysql, LifecycleMode.Root)) {
poolName = "myconnectionpool"
maximumPoolSize = 8
idleTimeout = 10000
}

Si cambias el modo de ciclo de vida de Spec, el contenedor no se iniciará en el constructor, por lo que cualquier operación que actúe sobre el contenedor debe colocarse dentro de los ámbitos de prueba.

Startables

Este módulo también proporciona métodos de extensión que te permiten convertir cualquier Startable (como un DockerContainer) en un TestListener de Kotest. Al registrarlo con Kotest, este gestionará su ciclo de vida.

Por ejemplo:

import io.kotest.core.spec.style.FunSpec
import io.kotest.extensions.testcontainers.perTest
import org.testcontainers.containers.GenericContainer

class DatabaseRepositoryTest : FunSpec({
val redisContainer = GenericContainer<Nothing>("redis:5.0.3-alpine")
listener(redisContainer.perTest()) //converts container to listener and registering it with Kotest.

test("some test which assume to have redis container running") {
//
}
})

En el ejemplo anterior, el método de extensión perTest() convierte el contenedor en un TestListener que inicia el contenedor Redis antes de cada test y lo detiene después. De forma similar, si quieres reutilizar el contenedor para todos los tests de una spec, puedes usar perSpec(), que convierte el contenedor en un TestListener que inicia el contenedor antes de ejecutar cualquier test en la spec y lo detiene después de todos los tests, usando un único contenedor para todas las pruebas de la clase.