Testcontainers
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Testcontainers
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:kotest-extensions-testcontainers:${kotest.version}
Desde Kotest 6.0, todas las extensiones se publican nuevamente bajo el grupo io.kotest, con una cadencia de versiones vinculada a
los lanzamientos principales de Kotest.
Para Maven, necesitarás estas dependencias:
<dependency>
<groupId>io.kotest</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(JdbcDatabaseContainerExtension(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(JdbcDatabaseContainerExtension(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"),
))
}
})
Esta extensión también admite la bandera ContainerLifecycleMode para controlar cuándo se inicia y detiene el contenedor.
Consulta Ciclo de vida
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: DataSource = install(JdbcDatabaseContainerExtension(mysql)) {
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 un elemento es una carpeta, procesará todos los scripts .sql en esa carpeta, ordenados lexicográficamente. Estos scripts se ejecutan cada vez que se inicia el contenedor, por lo que soporta la bandera ContainerLifecycleMode.
Contenedores generales
Similar a JdbcDatabaseContainerExtension, este módulo también proporciona una extensión ContainerExtension 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(ContainerExtension("redis:5.0.3-alpine")) {
startupAttempts = 1
withExposedPorts(6379)
}
Y luego usando un contenedor fuertemente tipado:
val elasticsearch = install(ContainerExtension(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.
Esta extensión también admite la bandera ContainerLifecycleMode para controlar cuándo se inicia y detiene el contenedor.
Consulta Ciclo de vida
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(ContainerExtension(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(ContainerExtension(KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")))) {
withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true")
}
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(ContainerExtension(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)
}
}
}
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 ContainerLifecycleMode a ContainerExtension o JdbcDatabaseContainerExtension.
Por ejemplo:
val ds = install(JdbcDatabaseContainerExtension(mysql, ContainerLifecycleMode.Spec)) {
poolName = "myconnectionpool"
maximumPoolSize = 8
idleTimeout = 10000
}
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, perTest() convierte el contenedor en un TestListener que inicia Redis antes de cada test y lo detiene después. Similarmente, para reutilizar el contenedor en todos los tests de un spec, usa perSpec(), que crea un TestListener iniciando el contenedor antes de cualquier test y deteniéndolo tras todos ellos, compartiendo un único contenedor en toda la clase spec.