Aserciones
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/assertions-kt
Puedes ejecutar el siguiente comando para crear el módulo
./gradlew setupAssertionsModule
Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.
import tasks.ModuleSetupTask
tasks.register<ModuleSetupTask>("setupAssertionsModule") {
description = "Creates the base module and files for the assertions lesson"
module.set("assertions")
doLast {
createFiles(
"calculator",
test to "CalculatorTest.kt",
main to "Calculator.kt",
)
createFiles(
"assertions",
test to "assertTrue.kt",
test to "assertIsOrdered.kt",
)
}
}
Preocúpate de que el plugin assertions
esté aplicado en el archivo build.gradle.kts
de tu proyecto.
./gradlew setupAssertionsModule
Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts
.
En esta lección aprenderás cómo implementar aserciones manuales y personalizadas, entendiendo cómo se comportan y cómo integrarlas con frameworks como Kotest. Esto sienta las bases para usar matchers más expresivos en lecciones futuras.
🧪 ¿Por qué necesitamos aserciones?
Al desarrollar bibliotecas o aplicaciones, una parte esencial del proceso es verificar que nuestro código haga lo que esperamos. Las aserciones son una de las herramientas más directas para lograr esto: son expresiones booleanas que validan condiciones críticas en tiempo de ejecución. Si la condición es verdadera, el programa continúa. Si es falsa, se lanza una excepción que interrumpe la ejecución y señala que algo se desvió del comportamiento esperado.
Esto convierte a las aserciones en una técnica valiosa dentro del testing funcional: permiten validar los invariantes del sistema, detectar errores al instante y reducir el tiempo necesario para depurar o corregir fallos.
✅ Beneficios clave del uso de aserciones
1. Detección temprana de errores
Las aserciones permiten capturar errores pequeños y sutiles que podrían pasar desapercibidos con otros métodos. Esto mejora la robustez del sistema y reduce el riesgo de propagar fallos a etapas más avanzadas del desarrollo.
2. Falla inmediata = feedback inmediato
En especial con aserciones duras (hard assertions), la ejecución se detiene justo en el punto donde algo falla. Esto facilita un ciclo de retroalimentación rápido: no se continúa evaluando condiciones irrelevantes una vez que algo crítico falla, permitiendo al equipo enfocarse directamente en el problema.
3. Verificación de garantías internas
Las aserciones también sirven como contratos internos dentro del código, ayudando a documentar y reforzar que ciertos bloques de lógica deben producir resultados correctos. Esto fortalece la autoconfianza del software al garantizar que sus partes clave se comportan de forma fiable.
🔍 ¿Cómo se clasifican las aserciones?
Tipos de aserciones y assertSoftly
assertSoftly
Las aserciones pueden clasificarse según su comportamiento al validar condiciones:
-
Aserciones simples: Verifican una única condición. Si falla, el test se interrumpe inmediatamente. Útiles para validar entradas, salidas o invariantes simples.
-
Aserciones compuestas: Agrupan varias condiciones en una misma evaluación. Todas deben cumplirse para que la prueba pase, permitiendo validar estructuras más complejas o múltiples propiedades al mismo tiempo.
-
Aserciones suaves (soft assertions): No interrumpen la ejecución tras el primer fallo. En lugar de eso, acumulan los errores y los reportan al final. Son ideales cuando se quiere tener una visión completa del estado del sistema, incluso si algunas partes fallan.
En Kotest, el mecanismo para realizar aserciones suaves es assertSoftly
, que agrupa varias verificaciones sin detener la ejecución ante la primera falla:
assertSoftly {
person.name shouldBe "Alice"
person.age shouldBe 30
person.email shouldContain "@"
}
Si una o más condiciones fallan, el framework mostrará todas las fallas juntas, facilitando el diagnóstico de errores sin necesidad de ejecutar múltiples veces.
Esta lección tiene fines didácticos y busca ilustrar cómo funcionan las aserciones.
En la práctica, en lugar de implementar nuestras propias aserciones, utilizamos matchers, que permiten escribir pruebas más expresivas y estructuradas.
En lecciones futuras, exploraremos el uso de matchers y cómo facilitan la validación en los tests.
📌 Implementación manual
Por ejemplo, consideremos una prueba escrita manualmente:
- Código esencial
- Código completo
"Given a calculator" - {
"when adding 1 and 2" - {
"should return 3" {
Calculator.add(1, 2).takeIf { it == 3 }
?: throw AssertionError("1 + 2 should be 3")
}
}
}
package com.github.username.calculator
import io.kotest.core.spec.style.FreeSpec
class CalculatorTest : FreeSpec({
"A calculator" - {
"when adding 1 and 2" - {
"should return 3 (manual)" {
Calculator.add(1, 2).takeIf { it == 3 }
?: throw AssertionError("1 + 2 should be 3")
}
}
}
})
En este código, verificamos si Calculator.add(1, 2)
es igual a 3
. Si no lo es, lanzamos una excepción AssertionError
con un mensaje descriptivo; de lo contrario, imprimimos "Test passed"
.
Supongamos ahora que tenemos la siguiente implementación errónea de Calculator
:
package com.github.username.calculator
object Calculator {
fun add(a: Int, b: Int): Int = a - b
}
En este caso, si ejecutamos el test obtendremos la siguiente salida:
java.lang.AssertionError: 1 + 2 should be 3
at com.github.username.assertions.CalculatorTest$1$1.invokeSuspend(CalculatorTest.kt:8)
at com.github.username.assertions.CalculatorTest$1$1.invoke(CalculatorTest.kt)
at com.github.username.assertions.CalculatorTest$1$1.invoke(CalculatorTest.kt)
at io.kotest.core.spec.style.scopes.FreeSpecRootScope$invoke$1.invokeSuspend(FreeSpecRootScope.kt:26)
...
Caused by: java.lang.AssertionError: 1 + 2 should be 3
at com.github.username.assertions.CalculatorTest$1$1.invokeSuspend(CalculatorTest.kt:8)
at com.github.username.assertions.CalculatorTest$1$1.invoke(CalculatorTest.kt)
at com.github.username.assertions.CalculatorTest$1$1.invoke(CalculatorTest.kt)
at io.kotest.core.spec.style.scopes.FreeSpecRootScope$invoke$1.invokeSuspend(FreeSpecRootScope.kt:26)
...
Sin embargo, podemos simplificar y mejorar este test utilizando las aserciones que ofrece el framework:
- Código esencial
- Código completo
Calculator.add(1, 2).takeIf { it == 3 }
?: fail("1 + 2 should be 3")
package com.github.username.calculator
import io.kotest.assertions.fail
import io.kotest.core.spec.style.FreeSpec
class CalculatorTest : FreeSpec({
"A calculator" - {
"when adding 1 and 2" - {
"should return 3" - {
Calculator.add(1, 2).takeIf { it == 3 }
?: fail("1 + 2 should be 3")
}
}
}
})
Aquí, en lugar de lanzar una excepción manualmente, utilizamos la función fail
proporcionada por el framework, que marca la prueba como fallida y proporciona el mensaje de error correspondiente. Esto también ayudará al framework a generar un informe detallado de la prueba.
🛠️ Implementación de Aserciones Personalizadas
Supongamos ahora que queremos implementar nuestras propias funciones de aserción personalizadas para simplificar la escritura de pruebas. Por ejemplo, podemos crear una función assertTrue
que verifique si una condición es verdadera y lance una excepción si no lo es.
package com.github.username.assertions
import io.kotest.assertions.fail
fun assertTrue(
condition: Boolean,
message: String = "Condition is not true"
) {
if (!condition) {
fail(message)
}
}
La función assertTrue
toma dos parámetros:
condition
: la condición que se debe cumplir.message
: el mensaje de error que se mostrará si la condición no se cumple.
Si la condición no se cumple, se lanza una excepción AssertionError
con el mensaje especificado.
🔥 Ejemplo de uso en tu prueba
- Código esencial
- Código completo
assertTrue(Calculator.add(1, 2) == 3, "1 + 2 should be 3")
package com.github.username.calculator
import com.github.username.assertions.assertTrue
import io.kotest.core.spec.style.FreeSpec
class CalculatorTest : FreeSpec({
"A calculator" - {
"when adding 1 and 2" - {
"should return 3" - {
assertTrue(Calculator.add(1, 2) == 3, "1 + 2 should be 3")
}
}
}
})
En este ejemplo, utilizamos la función assertTrue
para verificar si Calculator.add(1, 2)
es igual a 3
. Si la condición no se cumple, se lanza una excepción con el mensaje "1 + 2 should be 3"
.
⚖️ Beneficios y limitaciones
Beneficios
- Claridad y Reusabilidad: Las aserciones personalizadas mejoran la legibilidad del código de pruebas y permiten reutilizar las mismas aserciones en diferentes casos, reduciendo duplicaciones.
- Mensajes de error personalizados: Permite proporcionar mensajes de error más claros y específicos, lo que facilita la identificación de fallos en las pruebas.
- Abstracción de complejidad: Las aserciones personalizadas ocultan detalles complejos de validación, dejando las pruebas más concisas y fáciles de mantener.
- Integración con frameworks: Al crear aserciones personalizadas, se puede integrar fácilmente con frameworks de testing como Kotest, aprovechando su capacidad de generar informes detallados.
Limitaciones
- Mayor mantenimiento: Si las aserciones personalizadas no están bien diseñadas o documentadas, pueden ser difíciles de mantener y propensas a errores si los requisitos cambian.
- Sobrecarga inicial: Crear aserciones personalizadas desde cero puede llevar más tiempo y esfuerzo inicialmente, comparado con el uso de aserciones predefinidas de los frameworks.
🧠 Buenas prácticas
- Reutiliza funciones de aserción en varios tests para evitar duplicación.
- Incluye siempre mensajes de error descriptivos.
- Integra tus aserciones con el sistema del framework (como
fail()
en Kotest) para aprovechar sus reportes.
🔢 Ejercicio: Verificar si una lista está ordenada
Ejercicio
Supón que estás escribiendo una biblioteca que ordena rankings, y quieres asegurarte de que los resultados se ordenan correctamente antes de exponerlos a otrxs usuaries.
Crea una aserción assertIsOrdered: (List<Int>, Boolean) -> Unit
que verifique si una lista de enteros está ordenada de forma ascendente.
- Si el segundo parámetro es
true
, la lista debe estar ordenada de forma estrictamente ascendente (sin elementos repetidos). - Si el segundo parámetro es
false
, la lista puede contener elementos repetidos.
Solución
- Implementación iterativa
- Implementación funcional
fun assertIsOrdered(list: List<Int>, isStrictlyOrdered: Boolean) {
if (isStrictlyOrdered) {
for (i in 0..<list.size - 1) {
if (list[i] >= list[i + 1]) {
fail("List is not strictly ordered")
}
}
} else {
for (i in 0..<list.size - 1) {
if (list[i] > list[i + 1]) {
fail("List is not ordered")
}
}
}
}
fun assertIsOrdered(list: List<Int>, isStrictlyOrdered: Boolean) {
val comparator = if (isStrictlyOrdered) { a: Int, b: Int -> a < b }
else { a: Int, b: Int -> a <= b }
list.zipWithNext().all { (a, b) -> comparator(a, b) } ||
fail("List is not ordered")
}
zipWithNext()
crea una lista de pares consecutivos:[a1, a2, a3]
→[(a1, a2), (a2, a3)]
- Luego usamos
all { (a, b) -> comparator(a, b) }
para verificar que todos los pares cumplen la relación esperada (estricta o no estricta). - Si la condición no se cumple, lanzamos una excepción con el mensaje correspondiente.
🎯 Conclusiones
A lo largo de esta lección, exploramos el rol fundamental de las aserciones como mecanismo para validar el comportamiento del sistema en distintos puntos clave del código. Comenzamos con implementaciones manuales para comprender su funcionamiento esencial y luego avanzamos hacia la creación de funciones personalizadas que encapsulan lógica reusable.
Aunque hoy en día los frameworks modernos favorecen el uso de matchers más expresivos, comprender cómo funcionan las aserciones básicas nos permite:
- Entender qué ocurre "bajo el capó" en una prueba fallida,
- Diseñar validaciones adaptadas al dominio, y
- Construir herramientas propias cuando las existentes no son suficientes.
🔑 Puntos clave
- Una aserción es una validación que detiene la prueba si una condición no se cumple.
- Es posible implementar aserciones manualmente, pero frameworks como Kotest ofrecen utilidades como
fail()
yassertSoftly
para integrarse mejor con los reportes. - Las funciones de aserción personalizadas aumentan la expresividad, reducen duplicación y ayudan a expresar reglas del dominio.
- Mensajes claros facilitan la depuración cuando una prueba falla.
- Preparar funciones propias sienta las bases para comprender herramientas más potentes como matchers, validadores o DSLs de testing.
🧰 ¿Qué nos llevamos?
Aprender a escribir aserciones manuales nos obliga a pensar con precisión sobre qué se quiere validar, cómo y por qué. En el contexto del desarrollo de bibliotecas, esto es clave: las pruebas dejan de ser simples chequeos y se convierten en garantías explícitas del contrato que estamos definiendo para quienes usarán nuestro código.
Este conocimiento también te prepara para transicionar a herramientas más declarativas, como los matchers, que exploraremos en próximas lecciones. Así, ganamos control y flexibilidad para construir pruebas que sean no solo correctas, sino también claras, mantenibles y alineadas con el propósito de la biblioteca.
📖 Referencias
🔥 Recomendadas
📰 Tamas Cser. (2024, septiembre 5). What is Assertion Testing? Ensuring Quality and Accuracy. https://www.functionize.com/automated-testing/assertion
🔹 Adicionales
🌐 ¿Cuáles son las prácticas recomendadas para usar aserciones en las pruebas? (s. f.). Recuperado 23 de marzo de 2025, de https://es.linkedin.com/advice/0/what-best-practices-using-assertions-testing-6uxje?lang=es