Property-Based Testing
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/property-based-testing-kt
Si tienes gh
instalado, puedes obtener el código haciendo:
gh repo clone r8vnhill/property-based-testing-kt
cd property-based-testing-kt || exit
git checkout base
Si quieres tener tu propia copia del código, puedes hacer un fork del repositorio y clonarlo desde tu cuenta de GitHub.
gh repo fork r8vnhill/property-based-testing-kt
cd property-based-testing-kt || exit
git checkout --track origin/base
group
en gradle.properties
.group
en el archivo gradle.properties
por tu nombre de dominio.Testeando la función merge
Imaginemos que queremos testear la función merge
, la cual toma dos listas ordenadas como entrada y devuelve una nueva lista que contiene todos los elementos de ambas listas, manteniendo el orden.
¿Cómo podríamos probar esta función?
Podríamos recurrir a Data-Driven Testing para verificar su comportamiento. Esto implicaría definir varios conjuntos de datos de entrada y validar que la función produce los resultados esperados para cada uno. Sin embargo, ¿qué conjuntos de datos serían apropiados para cubrir todos los casos posibles?
Preguntas clave al elegir inputs
- ¿Qué ocurre si una o ambas listas están vacías?
- ¿Qué pasa cuando las listas tienen elementos repetidos?
- ¿Cómo se comporta
merge
cuando una lista contiene elementos mayores o menores que todos los de la otra? - ¿Qué sucede si ambas listas ya están ordenadas pero con diferentes rangos de valores?
Ejemplo de inputs que podríamos usar
merge([], [])
→[]
(Ambas listas vacías)merge([1, 3, 5], [])
→[1, 3, 5]
(Una lista vacía)merge([], [2, 4, 6])
→[2, 4, 6]
(Otra lista vacía)merge([1, 3], [2, 4])
→[1, 2, 3, 4]
(Listas con valores intercalados)merge([1, 2, 3], [4, 5, 6])
→[1, 2, 3, 4, 5, 6]
(Una lista con valores completamente menores)merge([2, 4, 6], [1, 3, 5])
→[1, 2, 3, 4, 5, 6]
(Listas desordenadas entre sí)
Definición de los Casos de Prueba con withData
A continuación, definimos los casos de prueba usando la estructura FreeSpec
y la función withData
, lo que permite pasar múltiples casos de prueba de forma concisa y ejecutar la misma lógica sobre diferentes inputs.
- Código esencial
- Código completo
withData(
Triple(listOf(), listOf(), listOf()),
Triple(listOf(1), listOf(), listOf(1)),
Triple(listOf(), listOf(1), listOf(1)),
Triple(listOf(1), listOf(1), listOf(1, 1)),
Triple(listOf(1, 2), listOf(1), listOf(1, 1, 2)),
Triple(listOf(1), listOf(1, 2), listOf(1, 1, 2)),
Triple(listOf(1, 3), listOf(2, 4), listOf(1, 2, 3, 4)),
Triple(listOf(1, 2, 3), listOf(4, 5, 6), listOf(1, 2, 3, 4, 5, 6))
) { (list1, list2, expected) ->
merge(list1, list2) shouldBe expected
}
package com.github.username.merge
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
class MergeTest : FreeSpec({
"Merging two lists" - {
"should return a list with all elements sorted" - {
withData(
MergeTestCase(listOf(), listOf(), listOf()),
MergeTestCase(listOf(1), listOf(), listOf(1)),
MergeTestCase(listOf(), listOf(1), listOf(1)),
MergeTestCase(listOf(1), listOf(1), listOf(1, 1)),
MergeTestCase(listOf(1, 2), listOf(1), listOf(1, 1, 2)),
MergeTestCase(listOf(1), listOf(1, 2), listOf(1, 1, 2)),
MergeTestCase(listOf(1, 3), listOf(2, 4), listOf(1, 2, 3, 4)),
MergeTestCase(listOf(1, 2, 3), listOf(4, 5, 6), listOf(1, 2, 3, 4, 5, 6)),
) { (list1, list2, expected) ->
merge(list1, list2) shouldBe expected
}
}
}
})
private data class MergeTestCase(
val list1: List<Int>,
val list2: List<Int>,
val expected: List<Int>
)
El Desafío del Data-Driven Testing
Aunque el Data-Driven Testing es una técnica poderosa para probar diferentes conjuntos de datos de manera sistemática, presenta algunos desafíos importantes:
Limitaciones
- Mantenimiento y complejidad: Requiere que quien desarrolla piense y defina manualmente todos los casos de prueba posibles. A medida que los escenarios crecen en complejidad, la cantidad de datos y la gestión de los mismos pueden volverse complicadas y propensas a errores.
- Dificultad para cumplir el Principio Open/Closed: Si necesitamos agregar más casos de prueba, esto generalmente implica modificar directamente el código de prueba existente, lo que puede ir en contra del principio Open/Closed del diseño de software. Este principio establece que el código debe estar abierto a la extensión pero cerrado a la modificación. Cada vez que agregamos nuevos casos, estamos alterando el cuerpo de las pruebas, lo que aumenta el riesgo de introducir errores o inconsistencias.
Posibles Soluciones
Una solución es utilizar generación automática de datos de prueba o emplear estrategias como el property-based testing, donde los casos se generan dinámicamente a partir de propiedades del sistema, reduciendo la necesidad de definir manualmente todos los casos y mejorando la capacidad de mantener el código de pruebas sin modificaciones constantes.
Property-Based Testing (PBT)
El Property-Based Testing es una metodología de prueba en la que se definen propiedades generales sobre el comportamiento esperado de un programa. En lugar de escribir casos de prueba específicos, se especifican propiedades que deben cumplirse para cualquier conjunto de entradas, y el framework de PBT genera automáticamente una amplia variedad de datos de entrada para verificar dichas propiedades.
Beneficios
- Cobertura exhaustiva: PBT permite probar una función contra miles de casos de prueba generados automáticamente, lo que aumenta la probabilidad de encontrar errores que podrían pasar desapercibidos con pruebas manuales o casos limitados.
- Detección temprana de errores: Al probar con una gama extensa de entradas, PBT ayuda a descubrir errores en etapas tempranas del desarrollo.
- Enfoque en el comportamiento: En lugar de enfocarse en casos específicos, PBT ayuda a definir y verificar el comportamiento general de un programa, asegurando que se cumpla bajo múltiples condiciones.
Limitaciones
- Mayor complejidad inicial: Escribir pruebas basadas en propiedades puede ser más complejo que los tests tradicionales, ya que se requiere una buena comprensión de las propiedades fundamentales del sistema que estamos probando.
Aunque puede ser más difícil de implementar al principio, Property-Based Testing ofrece una poderosa alternativa al enfoque tradicional de Data-Driven Testing. Con su capacidad para generar numerosos casos de prueba automáticamente, PBT no solo mejora la cobertura y la robustez de las pruebas, sino que también contribuye a identificar fallos más rápidamente.
¡Considera usar PBT como una herramienta preferida frente a DDT en tus proyectos!
Propiedades en Property-Based Testing
A diferencia de los tests tradicionales, que dependen de casos de prueba específicos basados en ejemplos, las propiedades en Property-Based Testing definen condiciones generales que deben cumplirse sin importar los valores de entrada de la función.
- Tests basados en ejemplos: Verifican comportamientos en función de inputs específicos.
- Testing basado en propiedades: Las propiedades establecen afirmaciones generales sobre el comportamiento que siempre deben cumplirse, independientemente de los inputs. El testing basado en propiedades se enfoca en verificar estas propiedades en lugar de casos de prueba individuales.
Importancia de la Pureza de las Propiedades
Para que las propiedades sean efectivas y fiables en Property-Based Testing, es crucial que sean propias (puras). La pureza de una propiedad implica que:
- Determinismo: Una propiedad pura siempre produce el mismo resultado dado el mismo input. No depende de estados externos ni de variables globales que puedan cambiar.
- Sin efectos secundarios: Las propiedades puras no modifican el estado del sistema ni interactúan con el mundo exterior (por ejemplo, no realizan operaciones de I/O, no modifican bases de datos, etc.).
- Independencia: Cada ejecución de una propiedad es independiente de las demás, lo que facilita la paralelización y evita interferencias entre pruebas.
Beneficios
- Reproducibilidad: Si una prueba falla, es más fácil reproducir el error porque no hay estados externos que afecten el comportamiento.
- Facilidad de Mantenimiento: Las propiedades puras son más simples de entender y mantener, ya que su comportamiento está completamente determinado por sus inputs.
- Eficiencia: Al no depender de efectos secundarios, las propiedades puras pueden paralelizarse de forma segura, lo que mejora la eficiencia de las pruebas.
Generación Automática de Casos de Prueba
El framework de testing genera automáticamente una variedad de casos de prueba basados en las propiedades definidas. Este proceso se basa en múltiples condiciones y combinaciones de inputs, asegurando que la función cumpla con las expectativas bajo distintas circunstancias. Al enfocarse en propiedades puras, se garantiza que cada caso de prueba sea independiente y confiable, maximizando la efectividad del testing.
Generadores arbitrarios
Los generadores arbitrarios son herramientas poderosas que permiten generar una gran variedad de entradas aleatorias para probar propiedades en los tests. En Kotest, existe una amplia gama de generadores prediseñados para diferentes tipos de datos, como enteros, cadenas, y colecciones, lo que facilita la creación de pruebas robustas con diversos escenarios de entrada.
Además, Kotest permite definir generadores personalizados, lo que brinda flexibilidad para casos de prueba más específicos y complejos. Estos generadores pueden componerse y transformarse, lo que los hace extremadamente versátiles para probar funciones bajo múltiples condiciones.
¿Qué aprendimos?
En esta lección exploramos dos enfoques fundamentales para el testing: Data-Driven Testing (DDT) y Property-Based Testing (PBT). Ambos métodos nos ayudan a validar el comportamiento de nuestras funciones, pero tienen enfoques y beneficios distintos.
Puntos clave
- Data-Driven Testing (DDT) permite un control explícito sobre los inputs, pero puede ser difícil de mantener y escalar, ya que los casos de prueba deben definirse manualmente y el código de prueba puede volverse complejo.
- Property-Based Testing (PBT) ofrece una alternativa poderosa al generar automáticamente casos de prueba, basándose en propiedades fundamentales del sistema bajo prueba. Aunque requiere más esfuerzo inicial para definir las propiedades, proporciona una cobertura mucho más exhaustiva y reduce la necesidad de mantener un gran número de casos de prueba predefinidos.
- Generadores Arbitrarios son una herramienta clave en PBT para crear una variedad de inputs, lo que asegura que las propiedades se prueben bajo condiciones diversas y difíciles de prever manualmente.
- Propiedades puras y su enfoque en el determinismo y la independencia de efectos secundarios garantizan que las pruebas sean fiables, reproducibles, y eficientes.
Al final, elegir entre DDT o PBT depende de las necesidades del proyecto. Si bien DDT es útil para pruebas controladas y específicas, PBT ofrece una ventaja considerable para pruebas exhaustivas y automatizadas, especialmente en funciones complejas o con muchas combinaciones de entradas posibles.
Bibliografías Recomendadas
- 📚 "Foundations of Property-Based Testing". (2019). Fred Hébert, en Property-based testing with PropEr, Erlang, and Elixir: Find bugs before your users do, (pp. 3–16.) The Pragmatic Bookshelf.