BDD by example
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/testing-kt
Puedes ejecutar el siguiente comando para crear el módulo
./gradlew setupBddModule
Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.
import tasks.ModuleSetupTask
tasks.register<ModuleSetupTask>("setupdBddModule") {
description = "Creates the base module and files for the BDD introductory lesson"
module.set("bdd")
doLast {
createFiles(
"users",
main to "UserService.kt",
test to "UserServiceTest.kt",
)
}
}
Preocúpate de que el plugin bdd
esté aplicado en el archivo build.gradle.kts
de tu proyecto.
./gradlew setupBddModule
Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts
.
El Desarrollo Dirigido por Comportamiento (Behavior-Driven Development, BDD) es una metodología ágil basada en colaboración. A diferencia del Desarrollo Dirigido por Pruebas (Test-Driven Development, TDD), BDD enfatiza la comunicación entre quienes programan, QA y partes interesadas no técnicas, asegurando que todas las personas involucradas compartan el mismo entendimiento del software. BDD se centra en el comportamiento esperado del software desde la perspectiva de quienes lo utilizan
En esta lección, exploraremos cómo implementar BDD utilizando Kotest. Suponemos que ya te manejas con TDD y ahora quieres integrar BDD en tu flujo de trabajo.
🚀 Introducción al BDD (Behavior-Driven Development)
El Behavior-Driven Development es una metodología que busca:
- Fomentar la colaboración entre todas las partes involucradas en el desarrollo, como equipos de desarrollo, testers, y stakeholders.
- Definir el comportamiento esperado del sistema utilizando un lenguaje claro y accesible, cercano al lenguaje natural.
- Guiar el desarrollo mediante ejemplos y escenarios específicos que ilustran cómo debería comportarse el sistema bajo diferentes circunstancias.
En BDD, las pruebas se estructuran para describir características y escenarios que se alinean con los requisitos del negocio, permitiendo que las pruebas actúen como documentación viviente del sistema.
Estructura de una Prueba BDD
Un test BDD típico sigue la siguiente estructura:
- Given: Describe el estado inicial o las condiciones previas del sistema.
- When: Describe la acción o evento que se está probando.
- Then: Describe el resultado o comportamiento esperado como consecuencia de la acción.
📚 Caso de Estudio: Gestión de Usuarios
Supongamos que estamos desarrollando una biblioteca de software para la gestión de usuarixs. Queremos implementar funcionalidades clave, como:
- Registrar nuevxs usuarixs en el sistema.
- Manejar situaciones excepcionales, como el intento de registro de unx usuarix que ya existe (duplicado).
Utilizaremos BDD para guiar el desarrollo de estas funcionalidades, escribiendo especificaciones que detallen el comportamiento esperado del sistema en diferentes escenarios.
✍️ Escribiendo Especificaciones con Kotest
Kotest proporciona varios estilos para escribir pruebas en un formato BDD. En esta lección, utilizaremos el estilo FreeSpec
, que es flexible y legible, permitiendo estructurar las pruebas en una jerarquía que imita el lenguaje natural y facilita la comprensión del flujo de escenarios. Aunque en esta lección utilizamos FreeSpec
, puedes optar por otros estilos según tus necesidades y preferencias.
✅ Registro de Usuarixs Exitoso
Especificación
Comenzamos escribiendo una especificación para un caso común en el que una persona se registra correctamente. La estructura de BDD con Kotest nos permite expresar el comportamiento de forma natural:
"A user service" - {
"when registering a new user" - {
"should add the user to the database" { }
}
}
Esta es la estructura básica de nuestra especificación BDD. Ahora, agregaremos los detalles que prueban si el usuario se registra correctamente en el sistema.
- Código esencial
- Código completo
lateinit var userService: UserService
beforeEach {
userService = UserService()
}
"A user service" - {
"when registering a new user" - {
"should add the user to the database" {
userService.register(USER, PASSWORD)
userService.users.contains(USER) shouldBe true
}
}
}
package com.github.username.users
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
private const val USER = "ichigo"
private const val PASSWORD = "shinigami123"
class UserServiceTest : FreeSpec({
lateinit var userService: UserService
// Inicializa el servicio antes de cada test
beforeEach {
userService = UserService()
}
"A user service" - {
"when registering a new user" - {
"should add the user to the database" {
userService.register(USER, PASSWORD)
userService.users.contains(USER) shouldBe true
}
}
}
})
- Estructura BDD: Hemos utilizado la sintaxis
given/when/then
implícita con el estiloFreeSpec
de Kotest. Esto facilita la escritura de pruebas de comportamiento enfocadas en casos de uso del sistema. lateinit
: La variableuserService
se declara comolateinit
, lo que significa que no se inicializa inmediatamente, sino antes de cada prueba. Esto es una alternativa más segura a la inicialización comonull
.- Inicialización: El servicio de usuarixs (
UserService
) se reinicializa antes de cada prueba para asegurar que cada prueba se ejecute en un entorno limpio. - Prueba esencial: La prueba verifica que, al registrar una nueva persona usuaria, esta se guarda correctamente en la "base de datos" (representada por una colección
users
).
Este enfoque BDD ayuda a que las pruebas reflejen el comportamiento esperado de la biblioteca de software desde una perspectiva clara y centrada en quien la utiliza.
Implementación Mínima
Ahora, implementamos el código mínimo para que la prueba pase.
package com.github.username.users
class UserService {
private val _users = mutableMapOf<String, String>()
val users: List<String>
get() = _users.keys.toList()
fun register(username: String, password: String) {
_users[username] = password
}
}
La prueba debe pasar ahora, confirmando que unx usuarix puede registrarse exitosamente.
Ejercicio
Extiene la clase UserService
para manejar la autenticación exitosa de unx usuarix. Escribe una especificación BDD y una prueba que verifique que unx usuarix registrado pueda autenticarse correctamente con su nombre de usuario y contraseña.
Solución
"when authenticating an existing user" - {
"should return true for valid credentials" {
userService.register(USER, PASSWORD)
userService.authenticate(USER, PASSWORD) shouldBe true
}
}
fun authenticate(username: String, password: String) =
_users[username] == password
⚠️ Manejo de Usuarixs Duplicados
Especificación
Escribimos una especificación para manejar el caso excepcional donde unx usuarix intenta registrarse con un nombre de usuario ya existente.
class UserServiceTest : FreeSpec({
"A user service" - {
"when registering a new user" - {
"should have the user in the database" { }
}
"when registering an existing user" - {
"should throw an exception" { }
}
}
})
Con el escenario definido, podemos llenar los detalles específicos.
- Código esencial
- Código completo
"when registering an existing user" - {
"should throw an exception" {
userService.register(USER, PASSWORD)
shouldThrow<IllegalArgumentException> {
userService.register(USER, PASSWORD)
}.message shouldBe "User already exists"
}
}
package cl.ravenhill.users
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
private const val USER = "ichigo"
private const val PASSWORD = "shinigami123"
class UserServiceTest : FreeSpec({
lateinit var userService: UserService
beforeEach {
userService = UserService()
}
"A user service" - {
"when registering a new user" - {
"should have the user in the database" {
userService.register(USER, PASSWORD)
userService.users.contains(USER) shouldBe true
}
}
"when registering an existing user" - {
"should throw an exception" {
userService.register(USER, PASSWORD)
shouldThrow<IllegalArgumentException> {
userService.register(USER, PASSWORD)
}.message shouldBe "User already exists"
}
}
}
})
- Prueba de excepción: Utilizamos
shouldThrow
para verificar que se arroje una excepciónIllegalArgumentException
cuando se intenta registrar unx usuarix duplicado. - Mensaje de error: Verificamos que el mensaje de la excepción sea
"User already exists"
, lo que indica que la persona ya está registrada en el sistema.
Implementación Mínima
Lo siguiente es implementar el código mínimo para que la prueba pase.
- Código esencial
- Código completo
fun register(username: String, password: String) {
require(username !in _users) { "User already exists" }
_users[username] = password
}
package com.github.username.users
class UserService {
private val _users = mutableMapOf<String, String>()
val users: List<String>
get() = _users.keys.toList()
fun register(username: String, password: String) {
require(username !in _users) { "User already exists" }
_users[username] = password
}
}
Ejercicio
Extiende la clase UserService
para manejar la autenticación fallida de unx usuarix. Escribe una especificación BDD y una prueba que verifique que unx usuarix no registrado no pueda autenticarse.
La autenticación puede fallar si la persona no existe en la base de datos o si la contraseña no coincide. Arroja una excepción IllegalArgumentException
con un mensaje adecuado en caso de autenticación fallida.
Solución
"when authenticating a non-existing user" - {
"should return false" {
val wrongPassword = "wrongpassword"
wrongPassword shouldNotBe PASSWORD
userService.register(USER, PASSWORD)
userService.authenticate(USER, "wrongpassword") shouldBe false
}
"should throw an exception" {
shouldThrow<IllegalArgumentException> {
userService.authenticate("nonexistent", "password")
}.message shouldBe "User not found"
}
}
fun authenticate(username: String, password: String): Boolean {
require(username in _users) { "User not found" }
return _users[username] == password
}
⚖️ Pros y Contras de BDD
Beneficios
- Colaboración mejorada: BDD promueve una comunicación clara entre desarrolladorxs, testers y stakeholders no técnicxs, asegurando que todxs compartan un entendimiento común de los requisitos y el comportamiento esperado del sistema.
- Documentación viviente: Las pruebas escritas en estilo BDD sirven como documentación viviente. Si el código cambia y una prueba BDD falla, eso indica que el comportamiento del sistema también ha cambiado, ayudando a detectar inconsistencias de inmediato.
- Enfoque en lx usuarix final: Al definir los escenarios en términos de comportamiento, BDD mantiene el enfoque en cómo el sistema debe funcionar desde la perspectiva de lx usuarix final, mejorando así la alineación con los objetivos del negocio.
- Mayor legibilidad: Las pruebas BDD escritas con Kotest o frameworks similares utilizan un lenguaje cercano al natural, lo que las hace más comprensibles para todas las partes involucradas en el proyecto, incluso aquellas sin conocimientos técnicos profundos.
- Prevención de errores: Al especificar los escenarios antes de implementar el código, BDD ayuda a identificar y manejar casos excepcionales de forma proactiva, lo que reduce la probabilidad de errores en tiempo de ejecución.
Limitaciones
- Curva de aprendizaje: Implementar BDD efectivamente puede requerir un cambio de mentalidad y aprendizaje de nuevas herramientas y técnicas, lo que podría ser un desafío para equipos que no estén familiarizados con esta metodología.
- Tiempo adicional: Escribir especificaciones detalladas y mantenerlas actualizadas puede llevar tiempo, lo que podría ralentizar el proceso de desarrollo inicial en comparación con otros enfoques menos estructurados.
- Dependencia de colaboración: El éxito de BDD depende en gran medida de la colaboración efectiva entre equipos técnicos y no técnicos. Si esta colaboración no es fluida, el proceso podría resultar ineficaz.
- Sobrecarga en proyectos pequeños: Para proyectos simples o de corta duración, el enfoque detallado y estructurado de BDD puede resultar excesivo y generar una sobrecarga innecesaria en comparación con TDD o pruebas unitarias tradicionales.
- Complejidad en casos complejos: En proyectos grandes con múltiples escenarios y flujos de negocio, la gestión de especificaciones BDD puede volverse compleja, requiriendo un enfoque disciplinado para mantener la claridad y la consistencia.
🎯 Conclusiones y Aprendizajes
En esta lección, exploramos cómo el Behavior-Driven Development (BDD), en combinación con Test-Driven Development (TDD), puede transformar la manera en que desarrollamos bibliotecas de software, especialmente en un lenguaje como Kotlin utilizando Kotest.
Puntos clave
- BDD no solo se enfoca en probar la funcionalidad, sino también en capturar y expresar el comportamiento esperado desde la perspectiva de lx usuarix final, utilizando un lenguaje claro y natural.
- La estructura Given-When-Then de BDD nos permite definir escenarios de manera ordenada y comprensible, alineando las pruebas con los requisitos del negocio.
- Integramos BDD en el desarrollo de una biblioteca de gestión de usuarixs, demostrando cómo se utilizan especificaciones para guiar la implementación y refactorización del código.
- Utilizando Kotest, exploramos cómo escribir pruebas que son legibles y estructuradas, facilitando tanto la colaboración en equipo como el mantenimiento a largo plazo del software.
Al final, BDD nos ayuda a mejorar la calidad del software, asegurando que cada característica se implemente con un claro entendimiento de su propósito y comportamiento esperado. Es una metodología que, aunque puede implicar una mayor inversión inicial en tiempo y esfuerzo, proporciona un valor significativo en términos de colaboración, claridad y reducción de errores a lo largo del ciclo de vida del desarrollo de software.