Skip to main content

Composición de Generadores

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/testing-kt

Be lazy...

Puedes ejecutar el siguiente comando para crear el módulo

./gradlew setupArbCompositionModule

Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.

import tasks.ModuleSetupTask

tasks.register<ModuleSetupTask>("setupArbCompositionModule") {
description = "Creates the base module and files for the arbitrary composition lesson"
module.set("pbt:arbitrary:composition")
doLast {
createFiles(
"forms",
main to "UserInput.kt",
test to "FormValidationTest.kt",
)
createFiles(
"config",
test to "ConfigGenerators.kt"
)
}
}

Preocúpate de que el plugin stats esté aplicado en el archivo build.gradle.kts de tu proyecto.

./gradlew setupArbCompositionModule

Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts.

En property-based testing (PBT), la clave es generar datos variados y realistas para verificar que una biblioteca de software funcione correctamente en distintos escenarios. En Behavior-Driven Development (BDD), primero definimos las expectativas sobre el comportamiento de la funcionalidad antes de implementar los generadores.

Kotest nos permite definir pruebas expresivas siguiendo BDD y generar datos complejos mediante composición de generadores. Esto es útil en bibliotecas de software que manejan estructuras configurables o datos extensibles.

📝 Definiendo los Tests Antes de los Generadores

Antes de crear los generadores, escribiremos los tests que definirán el comportamiento esperado de la biblioteca. Supongamos que estamos desarrollando una biblioteca de validación de formularios, donde validamos que los datos generados cumplan ciertas condiciones.

Usaremos Kotest con property-based testing para definir las siguientes pruebas:

checkAll(arbUserInput()) { user ->
user.name shouldHaveMinLength 3
}
checkAll(arbUserInput()) { user ->
user.email shouldMatch Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
}
checkAll(arbUserInput()) { user ->
(user.age shouldBeGreaterThanOrEqual 18) and
(user.age shouldBeLessThanOrEqual 99)
}

Aquí, cada prueba define una propiedad que debe cumplirse para todos los datos generados. Ahora, implementaremos los generadores para que los tests pasen.

🚀 Construyendo Generadores para Validaciones

Kotest proporciona herramientas para construir generadores reutilizables. Vamos a definir generadores para nombres, correos electrónicos y edades, asegurándonos de que cumplan con los requisitos de validación.

fun arbName(): Arb<String> = Arb.string(3..20)  // Nombres de mínimo 3 caracteres
fun arbEmail(): Arb<String> = Arb.stringPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}") // Emails válidos
fun arbAge(): Arb<Int> = Arb.int(18..99) // Edad dentro del rango permitido

Ahora, combinemos estos generadores en una sola estructura con Arb.bind:

fun arbUserInput(): Arb<UserInput> = Arb.bind(
arbName(),
arbEmail(),
arbAge()
) { name, email, age -> UserInput(name, email, age) }
¿Qué acabamos de hacer?

Arb.bind combina múltiples generadores en una estructura de datos compleja. En este caso, generamos un UserInput con un nombre, un correo electrónico y una edad válidos.

Con este generador, cada ejecución de arbUserInput() producirá un usuario con datos aleatorios pero válidos.

🔄 Generación de Datos en Colecciones

Si nuestra biblioteca maneja listas de usuarios, podemos definir un generador de listas:

fun arbUserList(): Arb<List<UserInput>> = Arb.list(arbUserInput(), 1..10)

Si queremos garantizar que los emails no se repitan en una lista de usuarios, usamos un conjunto:

fun arbUniqueUsers(): Arb<Set<UserInput>> = Arb.set(arbUserInput(), 1..10)

📚 Ejercicio Práctico: Generar Configuración de una Biblioteca

Ejercicio

Supongamos que estamos diseñando una biblioteca para gestionar configuraciones de una aplicación. Los usuarios pueden definir opciones de configuración, que pueden incluir valores de diferentes tipos, como:

  • String para nombres de usuario.
  • Boolean para opciones de activación/desactivación.
  • Int para límites numéricos.

💡 Objetivo: Crea un generador que produzca una lista de opciones de configuración. Cada opción debe ser representada como un par <clave, valor>, donde la clave es un String y el valor puede ser un String, Int o Boolean.

Ver hints
  • Utiliza el generador Arb.pair: (Arb<A>, Arb<B>) -> Arb<Pair<A, B>> para combinar generadores de claves y valores.
  • Utiliza Arb.choice: (Arb<A>, Arb<A>, ...) -> Arb<A> para elegir entre diferentes tipos de valores. Puedes considerar el tipo de A como un Any.
Solución
fun arbConfigOption(): Arb<Pair<String, Any>> = Arb.pair(
Arb.string(5..15), // Clave con longitud entre 5 y 15 caracteres
Arb.choice(Arb.string(1..20), Arb.int(1..100), Arb.boolean()) // Valores de diferentes tipos
)

fun arbConfigList(): Arb<List<Pair<String, Any>>> = Arb.list(arbConfigOption(), 3..10)

🎯 Conclusiones

La composición de generadores es una herramienta poderosa para diseñar pruebas expresivas y robustas en el desarrollo de bibliotecas de software. Al combinar generadores básicos en estructuras más complejas, no solo validamos el comportamiento esperado, sino que también acercamos nuestros tests a los escenarios reales de uso.

Este enfoque resulta especialmente útil cuando desarrollamos bibliotecas que operan sobre datos configurables, entradas dinámicas o estructuras heterogéneas. Gracias a herramientas como Kotest, podemos aplicar property-based testing de forma sencilla y declarativa, manteniendo los tests legibles, reutilizables y alineados con los principios de BDD.

🔑 Puntos clave

  • Composición con Arb.bind: Permite generar estructuras complejas combinando generadores simples.
  • Tests primero, generadores después: Al definir primero los tests, guiamos el diseño de nuestros generadores en función del comportamiento esperado.
  • Colecciones y restricciones: Podemos generar listas, conjuntos o mapas aplicando restricciones adicionales como unicidad o tamaño.
  • Adaptabilidad a distintos dominios: El patrón se aplica bien a bibliotecas de validación, configuración, procesamiento de datos, parsers, entre otros.

🧰 ¿Qué nos llevamos?

El uso de composición de generadores nos permite construir pruebas más expresivas, minimizar falsos positivos y detectar errores ocultos al cubrir una mayor variedad de escenarios. Esto no solo aumenta la calidad del software que entregamos, sino que también promueve un diseño más claro, modular y orientado a comportamiento.

Adoptar este enfoque en nuestras bibliotecas contribuye a un desarrollo más seguro y colaborativo, ya que los generadores también documentan las expectativas del dominio de manera precisa y reutilizable.

📖 Referencias

🔥 Recomendadas