Introducción
Hasta ahora has aprendido TDD, donde los tests guían el desarrollo. Pero hay un problema: los tests en código pueden ser difíciles de entender para personas no técnicas.
El Problema de Comunicación
Imagina que estás construyendo TaskFlow. Necesitas que:
- Los desarrolladores entiendan qué construir
- El equipo de QA sepa qué probar
- El quipo de negocio valide que se cumplen los requisitos
¿Cómo podemos comunicar los requisitos de forma que todos entiendan, y que además se pueda ejecutar como test?
Respuesta: BDD con Gherkin.
Video Recomendado
Para entender BDD con Behave y Selenium en Python:
Ver "BDD con Behave + Selenium + Python" Duración: 1:06:25 | Tutorial completo paso a pasoObjetivos de Aprendizaje
Al finalizar esta clase, serás capaz de:
- Comprender qué es Behavior Driven Development (BDD) y cuándo usarlo
- Diferenciar TDD de BDD y entender cómo se complementan
- Escribir escenarios en lenguaje Gherkin
- Implementar steps (pasos) en Python usando behave
- Ejecutar tests BDD y interpretar los resultados
- Aplicar el foco en valor de negocio al escribir requisitos
1. ¿Qué es BDD? (20 min)
Definición
Behavior Driven Development (BDD / Desarrollo Guiado por Comportamiento) es una metodología de desarrollo que:
- Fomenta la colaboración entre desarrolladores, QA y stakeholders de negocio
- Se enfoca en comportamientos que agregan valor al negocio
- Crea documentación viva que se ejecuta como tests
- Utiliza un lenguaje común comprensible por todos
1.1 Los Tres Pilares de BDD
Dev, QA y Negocio hablan el mismo idioma. Los requisitos se escriben juntos.
Cada feature debe responder: ¿Por qué? ¿Para quién? ¿Qué beneficio aporta?
Los escenarios BDD son documentación ejecutable. Siempre están actualizados.
1.2 Ejemplo: Antes y Después BDD
"El sistema debe validar que:
- El username tenga al menos 3 caracteres
- El email tenga formato válido
- El password tenga 8+ caracteres
- Llamar POST /api/usuarios
- Retornar 201 si OK, 400 si error"
❌ Solo desarrolladores entienden
Feature: Registro de Usuarios
Como visitante
Quiero registrarme en TaskFlow
Para poder gestionar mis tareas
Scenario: Registro exitoso
Given soy un visitante nuevo
When ingreso username "juan123"
And ingreso email válido "juan@email.com"
And confirmo el registro
Then mi cuenta es creada
And recibo email de confirmación
✅ Todos entienden, se puede ejecutar
2. TDD vs BDD: ¿Cuándo usar cada uno?
TDD y BDD no son competidores, se complementan. Aquí está la diferencia:
| Aspecto | TDD (Test-Driven Development) | BDD (Behavior-Driven Development) |
|---|---|---|
| Enfoque | Tests unitarios técnicos | Comportamiento del sistema desde el usuario |
| Lenguaje | Código Python (def test_*) | Gherkin (Given-When-Then) |
| Audiencia | Desarrolladores | Dev, QA, Negocio, Stakeholders |
| Nivel | Unitario (funciones, clases) | Integración/E2E (features completas) |
| Ejemplo | test_usuario_valido() |
Given usuario When registra Then éxito |
| Herramientas | pytest, unittest | behave, Cucumber, SpecFlow |
- BDD para features de alto nivel (lo que el usuario ve)
- TDD para implementación detallada (lo que el desarrollador construye)
Ejemplo: BDD define "Usuario puede registrarse" → TDD implementa validaciones, encriptación, etc.
TDD y BDD se Complementan
3. Lenguaje Gherkin - Especificaciones Comprensibles
¿Qué es Gherkin?
Gherkin es un DSL (Domain Specific Language / Lenguaje Específico del Dominio) que permite describir comportamientos del software en lenguaje natural estructurado. Es legible por humanos y ejecutable por máquinas.
3.1 Palabras Clave de Gherkin
| Palabra | Propósito | Ejemplo |
|---|---|---|
Feature |
Describe una funcionalidad completa | Feature: Login de usuarios |
Scenario |
Caso de prueba específico | Scenario: Login exitoso |
Given |
Contexto inicial (precondiciones) | Given un usuario registrado |
When |
Acción que realiza el usuario | When ingresa credenciales |
Then |
Resultado esperado (postcondiciones) | Then el login es exitoso |
And/But |
Añade pasos adicionales | And ve el dashboard |
Scenario Outline |
Template para múltiples casos | Scenario Outline: Login con |
Examples |
Datos para Scenario Outline | | usuario | password | |
Background |
Pasos comunes a todos los scenarios | Background: Usuario existe |
Estructura Given-When-Then
3.2 Estructura de un Feature Completo
Feature: Login en TaskFlow
Como usuario registrado
Quiero iniciar sesión con mis credenciales
Para acceder a mis proyectos y tareas
# Background: Pasos que se ejecutan antes de CADA scenario
Background:
Given el sistema está configurado
And la base de datos tiene usuarios de prueba
# Scenario: Caso de prueba específico
Scenario: Login exitoso con credenciales válidas
Given un usuario registrado con username "test" y password "pass123"
When ingresa username "test" y password "pass123"
Then el login es exitoso
And es redirigido al dashboard
And ve un mensaje "Bienvenido, test"
Scenario: Login fallido con password incorrecto
Given un usuario registrado con username "test" y password "pass123"
When ingresa username "test" y password "incorrecto"
Then el login falla
And ve el error "Credenciales inválidas"
And permanece en la página de login
Scenario: Login fallido con usuario inexistente
Given no existe un usuario con username "nouser"
When ingresa username "nouser" y password "cualquiera"
Then el login falla
And ve el error "Usuario no encontrado"
- Given: Establece el estado inicial (¿Qué ya existe?)
- When: La acción que desencadena el comportamiento (¿Qué hace el usuario?)
- Then: Los resultados observables (¿Qué cambió? ¿Qué ve el usuario?)
3.3 Scenario Outline: Parametrización en Gherkin
Cuando tienes múltiples casos similares con diferentes datos, usa Scenario Outline:
Feature: Validación de Registro de Usuarios
Scenario Outline: Validar nombres de proyecto
Given un usuario autenticado
When intenta crear proyecto con nombre ""
Then el sistema responde ""
And el código de respuesta es
Examples: Nombres válidos
| nombre | resultado | codigo |
| Proyecto Alpha | Proyecto creado | 201 |
| Mi Proyecto 2026 | Proyecto creado | 201 |
Examples: Nombres inválidos
| nombre | resultado | codigo |
| P | Nombre muy corto | 400 |
| | Nombre requerido | 400 |
| A | Nombre muy largo | 400 |
- El
Scenario Outlinees una plantilla con variables,, - La tabla
Examplesproporciona los valores para cada ejecución - El scenario se ejecuta una vez por cada fila de la tabla
- Puedes tener múltiples tablas Examples para agrupar casos
3.4 Tags: Organizando Scenarios
Puedes etiquetar scenarios para ejecutarlos selectivamente:
@regression @login
Feature: Login
@smoke @happy-path
Scenario: Login exitoso
Given ...
@error-handling
Scenario: Login con password incorrecto
Given ...
@performance @slow
Scenario: Login con verificación de 2FA
Given ...
# Ejecutar solo tests de smoke
behave --tags=smoke
# Ejecutar tests de smoke pero no los lentos
behave --tags=smoke --tags=~slow
# Ejecutar múltiples tags
behave --tags=login --tags=regression
4. behave Framework - De Gherkin a Python
behave es el framework de Python que ejecuta escenarios Gherkin como tests automatizados.
4.1 Instalación
# Instalar behave
pip install behave
# Verificar instalación
behave --version
# Plugins útiles
pip install behave-html-formatter # Reportes HTML
4.2 Estructura de Directorios
proyecto/
├── features/
│ ├── login.feature # Escenarios en Gherkin
│ ├── registro.feature # Otro feature
│ └── steps/
│ ├── login_steps.py # Implementación de pasos
│ ├── registro_steps.py # Más pasos
│ └── __init__.py
├── src/
│ └── ... # Código de la aplicación
└── behave.ini # Configuración (opcional)
4.3 Implementando Steps en Python
Cada línea Gherkin (Given, When, Then) debe tener una implementación Python:
# features/steps/login_steps.py
from behave import given, when, then
import requests
# Diccionario para simular nuestra "base de datos"
auth_service = {
"usuarios": {},
"sesiones": {}
}
# ============================================
# GIVEN: Establecer el contexto inicial
# ============================================
@given('un usuario registrado con username "{username}" y password "{password}"')
def step_given_usuario_registrado(context, username, password):
"""
Registra un usuario para usarlo en los tests.
El decorador @given recibe un patrón regex que coincide con el texto Gherkin.
Las variables {username} y {password} se capturan automáticamente.
"""
auth_service["usuarios"][username] = {
"password": password,
"activo": True
}
# Guardamos en context para usar en otros pasos
context.username = username
context.password = password
print(f"\n[SETUP] Usuario '{username}' registrado")
@given('no existe un usuario con username "{username}"')
def step_given_usuario_no_existe(context, username):
"""Asegura que el usuario no exista."""
if username in auth_service["usuarios"]:
del auth_service["usuarios"][username]
context.username = username
@given('el sistema está configurado')
def step_given_sistema_configurado(context):
"""Setup general del sistema."""
context.api_url = "http://localhost:8000/api"
context.headers = {"Content-Type": "application/json"}
# ============================================
# WHEN: Ejecutar la acción
# ============================================
@when('ingresa username "{username}" y password "{password}"')
def step_when_login(context, username, password):
"""Simula el intento de login."""
# Simulamos una llamada al servicio de autenticación
stored_user = auth_service["usuarios"].get(username)
if not stored_user:
context.resultado = {
"exitoso": False,
"error": "Usuario no encontrado"
}
elif stored_user["password"] != password:
context.resultado = {
"exitoso": False,
"error": "Credenciales inválidas"
}
else:
# Login exitoso
token = f"token_{username}_123"
auth_service["sesiones"][token] = username
context.resultado = {
"exitoso": True,
"token": token,
"username": username
}
@when('confirma el registro')
def step_when_confirma_registro(context):
"""Confirma el registro del usuario."""
# Implementación del registro
pass
# ============================================
# THEN: Verificar resultados
# ============================================
@then('el login es exitoso')
def step_then_login_exitoso(context):
"""Verifica que el login fue exitoso."""
assert context.resultado["exitoso"] is True, \
f"Esperado exitoso=True, obtenido: {context.resultado}"
@then('el login falla')
def step_then_login_falla(context):
"""Verifica que el login falló."""
assert context.resultado["exitoso"] is False, \
f"Esperado exitoso=False, obtenido: {context.resultado}"
@then('ve el error "{mensaje}"')
def step_then_ve_error(context, mensaje):
"""Verifica el mensaje de error."""
error_actual = context.resultado.get("error", "")
assert mensaje in error_actual, \
f"Esperado error contenga '{mensaje}', obtenido: '{error_actual}'"
@then('es redirigido al dashboard')
def step_then_redirigido_dashboard(context):
"""Verifica redirección al dashboard."""
assert "token" in context.resultado, "No se generó token"
print(f"\n[CHECK] Redirigido al dashboard con token: {context.resultado['token']}")
@then('permanece en la página de login')
def step_then_permanece_login(context):
"""Verifica que no hubo redirección."""
assert "token" not in context.resultado, \
"No debería haber token en login fallido"
@then('ve un mensaje "{mensaje}"')
def step_then_ve_mensaje(context, mensaje):
"""Verifica mensaje de bienvenida u otro."""
# En implementación real, verificarías la UI o respuesta API
pass
4.4 Ejecutar behave
# Ejecutar todos los features
behave
# Ejecutar un feature específico
behave features/login.feature
# Ejecutar con formato detallado
behave --format pretty --color
# Ejecutar un scenario específico (por línea)
behave features/login.feature:15
# Mostrar todos los pasos, incluso los que pasan
behave --verbose
# Generar reporte HTML
behave --format html --outfile=report.html
# Ver scenario sin ejecutar (dry run)
behave --dry-run
4.5 Salida de behave
Feature: Login en TaskFlow
Como usuario registrado
Quiero iniciar sesión con mis credenciales
Para acceder a mis proyectos y tareas
Background: ...
Scenario: Login exitoso con credenciales válidas
Given un usuario registrado with username "test" ... PASSED
When ingresa username "test" y password "pass123" ... PASSED
Then el login es exitoso ... PASSED
And es redirigido al dashboard ... PASSED
And ve un mensaje "Bienvenido, test" ... PASSED
Scenario: Login fallido con password incorrecto
Given un usuario registrado with username "test" ... PASSED
When ingresa username "test" y password "incorrecto" ... PASSED
Then el login falla ... PASSED
And ve el error "Credenciales inválidas" ... PASSED
And permanece en la página de login ... PASSED
1 feature passed, 0 failed, 0 skipped
5. Ejemplos Completos
5.1 Feature: Gestión de Tareas
Feature: Gestión de Tareas
Como usuario de TaskFlow
Quiero crear y gestionar tareas
Para organizar mi trabajo
Background:
Given soy usuario autenticado como "juan"
And tengo un proyecto llamado "Mi Proyecto"
Scenario: Crear tarea exitosamente
When creo una tarea con:
| campo | valor |
| titulo | Implementar login |
| descripcion | Usar JWT |
| prioridad | alta |
Then la tarea se crea exitosamente
And aparece en la lista de tareas
And tiene estado "pendiente"
Scenario: Completar una tarea
Given existe una tarea "Hacer tests" en el proyecto
When marco la tarea como completada
Then la tarea muestra estado "completada"
And la fecha de finalización se registra
from behave import given, when, then
from dataclasses import dataclass
@dataclass
class Tarea:
titulo: str
descripcion: str
prioridad: str
estado: str = "pendiente"
# Context storage
tareas_db = []
@given('soy usuario autenticado como "{username}"')
def step_usuario(context, username):
context.usuario = {"username": username}
@given('tengo un proyecto llamado "{nombre}"')
def step_proyecto(context, nombre):
context.proyecto = {
"nombre": nombre,
"tareas": []
}
@when('creo una tarea con:')
def step_crear_tarea(context):
# behave convierte la tabla en context.table
datos = {row['campo']: row['valor']
for row in context.table}
context.tarea = Tarea(**datos)
context.proyecto["tareas"].append(context.tarea)
@then('la tarea se crea exitosamente')
def step_tarea_creada(context):
assert context.tarea is not None
assert context.tarea.titulo is not None
@then('aparece en la lista de tareas')
def step_tarea_en_lista(context):
assert context.tarea in context.proyecto["tareas"]
@then('tiene estado "{estado}"')
def step_estado_tarea(context, estado):
assert context.tarea.estado == estado
5.2 Feature: API REST con TestClient
# features/steps/api_steps.py
from behave import given, when, then
from fastapi.testclient import TestClient
from taskflow.main import app
client = TestClient(app)
@given('la API está ejecutándose')
def step_api_running(context):
context.client = client
context.base_url = "/api/v1"
@when('hago GET a "{endpoint}"')
def step_get(context, endpoint):
context.response = context.client.get(
f"{context.base_url}{endpoint}"
)
@when('hago POST a "{endpoint}" con:')
def step_post(context, endpoint):
data = {row['campo']: row['valor']
for row in context.table}
context.response = context.client.post(
f"{context.base_url}{endpoint}",
json=data
)
@then('la respuesta tiene status {codigo:d}')
def step_status_code(context, codigo):
assert context.response.status_code == codigo, \
f"Esperado {codigo}, obtenido {context.response.status_code}"
@then('la respuesta contiene "{campo}"')
def step_response_contains(context, campo):
data = context.response.json()
assert campo in data, \
f"Campo '{campo}' no encontrado en respuesta"
@then('el campo "{campo}" es "{valor}"')
def step_field_equals(context, campo, valor):
data = context.response.json()
assert str(data.get(campo)) == valor, \
f"Esperado {campo}='{valor}', obtenido '{data.get(campo)}'"
Feature: API de Tareas
Scenario: Listar tareas vacías
Given la API está ejecutándose
When hago GET a "/tareas"
Then la respuesta tiene status 200
And la respuesta es una lista vacía
Scenario: Crear nueva tarea
Given la API está ejecutándose
When hago POST a "/tareas" con:
| campo | valor |
| titulo | Nueva tarea |
| descripcion | Descripción |
Then la respuesta tiene status 201
And la respuesta contiene "id"
And la respuesta contiene "titulo"
And el campo "titulo" es "Nueva tarea"
Ejercicio: Feature de Registro con behave
Crea un feature completo para el registro de usuarios en TaskFlow.
Requisitos
- Crear archivo
features/registro.feature - Crear archivo
features/steps/registro_steps.py - Al menos 5 scenarios:
- Registro exitoso
- Email ya registrado
- Username muy corto
- Password débil
- Campos vacíos
- Usar Scenario Outline para validaciones
Feature Sugerida
Feature: Registro de Usuarios
Como visitante de TaskFlow
Quiero crear una cuenta
Para poder gestionar mis proyectos
Scenario: Registro exitoso
Given soy un visitante nuevo
When ingreso los siguientes datos:
| campo | valor |
| username | juanperez |
| email | juan@email.com |
| password | Secreto123! |
| confirm_password | Secreto123! |
And confirmo el registro
Then mi cuenta es creada exitosamente
And recibo un email de confirmación
And veo el mensaje "Registro exitoso"
Scenario Outline: Validaciones de registro
When intento registrar con username "", email "", password ""
Then el registro falla con error ""
Examples:
| username | email | password | error |
| jp | jp@email.com | Pass123! | Username muy corto |
| juanp | email-invalido | Pass123! | Email inválido |
| juanp | jp@email.com | 123 | Password muy débil |
| juanp | jp@email.com | | Password requerido |
Steps Sugeridos
# features/steps/registro_steps.py
from behave import given, when, then
import re
# Simulamos base de datos
usuarios_registrados = {}
@given('soy un visitante nuevo')
def step_visitante_nuevo(context):
context.visitante = {"nuevo": True}
@when('ingreso los siguientes datos:')
def step_ingresa_datos(context):
context.datos = {
row['campo']: row['valor']
for row in context.table
}
@when('confirmo el registro')
def step_confirma_registro(context):
# Validaciones
username = context.datos.get('username', '')
email = context.datos.get('email', '')
password = context.datos.get('password', '')
errores = []
if len(username) < 3:
errores.append("Username muy corto")
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
errores.append("Email inválido")
if len(password) < 6:
errores.append("Password muy débil")
if email in usuarios_registrados:
errores.append("Email ya registrado")
if errores:
context.resultado = {
"exitoso": False,
"errores": errores
}
else:
usuarios_registrados[email] = context.datos
context.resultado = {
"exitoso": True,
"mensaje": "Registro exitoso"
}
@then('mi cuenta es creada exitosamente')
def step_cuenta_creada(context):
assert context.resultado["exitoso"] is True
@then('recibo un email de confirmación')
def step_email_confirmacion(context):
# Simulación
pass
@then('veo el mensaje "{mensaje}"')
def step_ve_mensaje(context, mensaje):
assert context.resultado.get("mensaje") == mensaje
@when('intento registrar con username "{username}", email "{email}", password "{password}"')
def step_intenta_registrar(context, username, email, password):
context.datos = {
"username": username,
"email": email,
"password": password
}
# Reutilizamos la lógica de confirmar
context.execute_steps('When confirmo el registro')
@then('el registro falla con error "{error}"')
def step_registro_falla(context, error):
assert context.resultado["exitoso"] is False
assert error in context.resultado["errores"]
- Al menos 3 archivos .feature
- Al menos 15 scenarios en total
- Usar Scenario Outline al menos 2 veces
- Todos los scenarios deben ejecutarse sin errores
- Documentar en README cómo ejecutar los tests BDD
Resumen y Checklist
Conceptos Clave
- BDD: Desarrollo guiado por comportamiento
- Colaboración: Dev, QA y Negocio trabajan juntos
- Gherkin: Lenguaje Given-When-Then
- Feature: Describe funcionalidad completa
- Scenario: Caso de prueba específico
- Steps: Implementación Python
- behave: Framework para ejecutar BDD en Python
- Scenario Outline: Parametrización de scenarios
Comandos Útiles
# Instalar
pip install behave
# Ejecutar
behave
behave features/login.feature
behave --format pretty
behave --tags=smoke
# Ver scenario
behave --dry-run
- Usa BDD para escenarios de usuario (features de alto nivel)
- Usa TDD para implementación detallada (unit tests)
- Los tests BDD verifican que "el sistema hace lo correcto"
- Los tests TDD verifican que "el código funciona correctamente"