IF0100 - Programación OO II

Unidad 2: Técnicas de Desarrollo

Clase 3: Introducción a BDD - Behavior Driven Development

Martes, 10 de marzo de 2026 Semana 6 - Martes (120 minutos) 60 min Teoría 60 min Práctica

E2: Taller TDD/BDD - Jueves 12/03/2026 (2 días)

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 paso

Objetivos 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

Colaboración

Dev, QA y Negocio hablan el mismo idioma. Los requisitos se escriben juntos.

Valor de Negocio

Cada feature debe responder: ¿Por qué? ¿Para quién? ¿Qué beneficio aporta?

Documentación Viva

Los escenarios BDD son documentación ejecutable. Siempre están actualizados.

1.2 Ejemplo: Antes y Después BDD

Antes (Requisito Técnico)
"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

Después (BDD con Gherkin)
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
Estrategia Combinada (Recomendada):
  1. BDD para features de alto nivel (lo que el usuario ve)
  2. 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
TDD Tests Unitarios Desarrolladores BDD Comportamiento Todo el equipo ✅ Calidad ✅ Tests ✅ Diseño Usa ambos para máxima cobertura

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
GIVEN Contexto Inicial (Precondiciones) WHEN Acción/Evento (El usuario hace...) THEN Resultado Esperado (Verificaciones)

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"
Anatomía de un Buen Scenario:
  • 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    |
¿Cómo funciona?
  1. El Scenario Outline es una plantilla con variables , ,
  2. La tabla Examples proporciona los valores para cada ejecución
  3. El scenario se ejecuta una vez por cada fila de la tabla
  4. 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
El objeto context: Es un objeto compartido entre todos los steps de un scenario. Úsalo para pasar datos entre Given → When → Then. Por ejemplo: guardar IDs, tokens, respuestas API, etc.

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

Gherkin
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
Python (Steps)
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

  1. Crear archivo features/registro.feature
  2. Crear archivo features/steps/registro_steps.py
  3. Al menos 5 scenarios:
    • Registro exitoso
    • Email ya registrado
    • Username muy corto
    • Password débil
    • Campos vacíos
  4. 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"]
E2 - Requisitos BDD:
  • 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
Enfoque Híbrido Recomendado:
  • 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"
Videos Recomendados
Introducción a BDD
Ver "Desarrollo dirigido por comportamiento" 5:43 min | NullSafe Architect
Cucumber Básico
Ver "Tu primer test en BDD" 2:20:03 | Curso completo de BDD
← Anterior: pytest Avanzado
Clase 9 de 25
Siguiente: DDD →