TDD, BDD y Gherkin: guia practica para entenderlos y escribirlos bien

Iniciado por ANTRAX, Septiembre 13, 2026, 06:36:12 PM

Tema anterior - Siguiente tema

0 Miembros y 1 Visitante están viendo este tema.

Septiembre 13, 2026, 06:36:12 PM Ultima modificación: Septiembre 13, 2026, 06:38:54 PM por ANTRAX Razón: Correccion de formato BBCode
TDD, BDD y Gherkin: guía práctica para entenderlos y escribirlos bien

Buenas gente. En el You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login quedó picando un tema que genera tanta o más confusión: la relación entre TDD, BDD y Gherkin. Es muy común escuchar "nosotros hacemos BDD" cuando en realidad lo único que se hizo fue escribir archivos .feature, o "TDD es escribir tests" a secas. Ninguna de las dos cosas es exacta.

Voy a separar los tres conceptos, mostrar cómo se encadenan, y sobre todo dar ejemplos de código reales: el mismo requisito recorriendo TDD, después BDD, y finalmente escrito en Gherkin con sus step definitions. Al final dejo reglas concretas para escribir buen Gherkin, antipatrones, una tabla comparativa y el stack de herramientas por lenguaje.



1. Lo primero: ninguno de los tres es "una forma de testear"

Este es el malentendido de base y vale despejarlo antes de cualquier otra cosa.

  • TDD es una técnica de diseño de software. Los tests son un efecto secundario, no el objetivo.
  • BDD es una técnica de colaboración y descubrimiento de requisitos. La automatización es la última etapa, no la primera.
  • Gherkin no es una metodología: es un lenguaje, una sintaxis. Es la herramienta con la que se escribe una parte de BDD.

Ponerlos en la misma categoría es como comparar "diseño orientado a objetos", "reuniones de refinamiento" y "UML". Se relacionan, pero operan en planos distintos. Si te quedás sólo con esto, ya vale el post.



2. TDD — Test Driven Development

TDD nace del Extreme Programming, popularizado por Kent Beck a fines de los 90. La idea central es contraintuitiva: escribís la prueba antes que el código de producción.

El ciclo es Red-Green-Refactor:


  • Red: escribís una prueba que falla. Tiene que fallar, y tenés que verla fallar. Si pasa a la primera, la prueba no está probando nada.
  • Green: escribís el mínimo código necesario para que pase. El mínimo, literal. Hardcodear un valor es válido en esta etapa.
  • Refactor: con la red de seguridad en verde, mejorás el diseño sin cambiar el comportamiento.

Robert C. Martin lo resumió en tres reglas:

Citar1. No escribas código de producción salvo para hacer pasar una prueba que falla.
2. No escribas más de una prueba de la necesaria para fallar (no compilar es fallar).
3. No escribas más código de producción del necesario para pasar la prueba.

Un ejemplo completo, paso a paso. Requisito: una función que calcule el descuento de un carrito según el monto total.

Iteración 1 — Red. La prueba primero:

Código: python
# tests/test_descuentos.py
from app.descuentos import calcular_descuento


def test_sin_descuento_por_debajo_de_1000():
    assert calcular_descuento(999) == 0

Corremos y falla, porque el módulo ni siquiera existe:

Código: bash
$ pytest -q
E   ModuleNotFoundError: No module named 'app.descuentos'
1 error in 0.04s

Green. El mínimo absoluto:

Código: python
# app/descuentos.py
def calcular_descuento(total):
    return 0

Sí, devuelve 0 siempre. Está bien: es el mínimo que hace pasar la prueba. Verde.

Iteración 2 — Red. Agregamos el siguiente comportamiento:

Código: python
def test_diez_por_ciento_entre_1000_y_5000():
    assert calcular_descuento(1000) == 100
    assert calcular_descuento(4999) == 499.9

Falla. Green:

Código: python
def calcular_descuento(total):
    if total < 1000:
        return 0
    return total * 0.10

Iteración 3 — Red. Tramo superior:

Código: python
def test_veinte_por_ciento_desde_5000():
    assert calcular_descuento(5000) == 1000
    assert calcular_descuento(12000) == 2400


def test_monto_negativo_es_invalido():
    import pytest
    with pytest.raises(ValueError):
        calcular_descuento(-1)

Green:

Código: python
def calcular_descuento(total):
    if total < 0:
        raise ValueError("El total no puede ser negativo")
    if total < 1000:
        return 0
    if total < 5000:
        return total * 0.10
    return total * 0.20

Refactor. Ahora que está verde, mejoramos el diseño. Los tramos son datos, no lógica:

Código: python
from decimal import Decimal

# Tramos: (monto_minimo, porcentaje). Ordenados de mayor a menor.
TRAMOS = (
    (Decimal("5000"), Decimal("0.20")),
    (Decimal("1000"), Decimal("0.10")),
    (Decimal("0"),    Decimal("0.00")),
)


def calcular_descuento(total):
    total = Decimal(str(total))
    if total < 0:
        raise ValueError("El total no puede ser negativo")

    for minimo, porcentaje in TRAMOS:
        if total >= minimo:
            return total * porcentaje

    return Decimal("0")

Las pruebas siguen en verde, y ahora agregar un tramo nuevo es agregar una tupla, no un if. Ese cambio de diseño lo pudimos hacer sin miedo porque las pruebas existían antes. Esa es la ganancia real de TDD: no son los tests, es que el código termina siendo testeable, desacoplado y modificable, porque lo diseñaste desde el punto de vista de quien lo consume.

El ciclo completo, visto a mayor escala, se ve así:


Lo que TDD no resuelve. Y acá está el puente al próximo tema. TDD garantiza que construiste bien la cosa. No garantiza que hayas construido la cosa correcta. Podés tener cobertura del 100%, todo verde, y haber implementado un requisito que Producto nunca pidió. TDD es una conversación del desarrollador consigo mismo.



3. BDD — Behaviour Driven Development

BDD surge en 2006 de la mano de Dan North. El origen es casi anecdótico: North notaba que la palabra "test" confundía a la gente que aprendía TDD. Los desarrolladores no sabían por dónde empezar, qué probar o cuán grande debía ser cada prueba.

Su solución fue cambiar el vocabulario. En vez de test, behaviour (comportamiento). En vez de nombrar los métodos testX, nombrarlos should.... Ese cambio aparentemente cosmético tuvo un efecto enorme: cuando dejás de pensar "¿qué testeo?" y empezás a pensar "¿cómo debería comportarse esto?", la respuesta la puede dar alguien de negocio.

BDD es, ante todo, una práctica de colaboración. Tiene tres etapas, y esto es lo que más se saltea:

  • Discovery (descubrimiento): una conversación estructurada entre negocio, desarrollo y testing para entender qué se necesita. Acá aparecen los casos borde que nadie había pensado. Es la etapa más valiosa y la que más gente omite.
  • Formulation (formulación): escribir esos ejemplos concretos en un lenguaje estructurado y compartido. Acá entra Gherkin.
  • Automation (automatización): convertir esos ejemplos en pruebas ejecutables.

La conversación de Discovery se suele hacer con la técnica de los Three Amigos: tres perspectivas sobre la misma historia.

Código: text
   +---------------------+   "que problema
   |  Negocio / Producto |    queremos resolver?"
   +----------+----------+
              |
   +----------+----------+   "como lo
   |     Desarrollo      |    construimos?"
   +----------+----------+
              |
   +----------+----------+   "que pasa si...?"
   |    QA / Testing     |    (casos borde)
   +---------------------+

El QA en esa mesa no está para probar después: está para romper el requisito antes de que se escriba. "¿Qué pasa si el usuario no tiene saldo?" "¿Qué pasa si la tarjeta vence entre que la cargó y confirmó?" Cada una de esas preguntas, hecha en Discovery, ahorra un bug en producción.

El doble loop. La relación entre BDD y TDD se entiende mejor así: BDD envuelve a TDD. Hay un loop externo de comportamiento y uno interno de implementación.

Código: text
   LOOP EXTERNO (BDD / comportamiento - lenguaje de negocio)
   +----------------------------------------------------------+
   |                                                          |
   |   escribir escenario  -->  escenario en ROJO             |
   |          ^                        |                      |
   |          |                        v                      |
   |          |        LOOP INTERNO (TDD / unidades)          |
   |          |        +------------------------------+       |
   |          |        | test rojo -> codigo -> verde |       |
   |          |        |       ^                |     |       |
   |          |        |       +---- refactor <-+     |       |
   |          |        +------------------------------+       |
   |          |                        |                      |
   |          +---- escenario en VERDE +                      |
   |                                                          |
   +----------------------------------------------------------+

Empezás por afuera con un escenario en rojo, bajás al loop interno de TDD y hacés varios ciclos rojo-verde-refactor de unidades hasta que el escenario externo se pone verde. Después volvés a subir y escribís el siguiente escenario.



4. Gherkin — el lenguaje

Gherkin es el DSL (lenguaje de dominio específico) que se usa en la etapa de Formulation. Está diseñado para ser legible por alguien que no programa, y a la vez parseable por una máquina. Soporta más de 70 idiomas, español incluido.

Anatomía de un archivo .feature:

Código: text
  Feature:          titulo y descripcion del conjunto de comportamientos
    |
    +-- Rule:       (opcional, Gherkin 6+) agrupa escenarios por regla de negocio
    |
    +-- Background: pasos comunes que corren antes de CADA escenario
    |
    +-- Scenario:   un ejemplo concreto de comportamiento
    |     |
    |     +-- Given  contexto / estado inicial
    |     +-- When   la accion que dispara el comportamiento
    |     +-- Then   el resultado esperado (observable)
    |     +-- And / But   encadenan al paso anterior
    |
    +-- Scenario Outline: plantilla parametrizada
          +-- Examples: tabla de datos que la alimenta

Un ejemplo realista y completo. Transferencias bancarias:

Código: gherkin
# language: es
@transferencias @critico
Característica: Transferencias entre cuentas propias

  Como cliente del banco
  Quiero transferir dinero entre mis cuentas
  Para administrar mis fondos sin ir a una sucursal

  Antecedentes:
    Dado que estoy autenticado como "mlopez"
    Y tengo una caja de ahorro "CA-001" con saldo de 50000 pesos
    Y tengo una cuenta corriente "CC-002" con saldo de 10000 pesos

  Escenario: Transferencia exitosa entre cuentas propias
    Cuando transfiero 20000 pesos desde "CA-001" hacia "CC-002"
    Entonces la operación se confirma
    Y el saldo de "CA-001" es 30000 pesos
    Y el saldo de "CC-002" es 30000 pesos
    Y recibo un comprobante con número de operación

  Escenario: Se rechaza la transferencia por saldo insuficiente
    Cuando transfiero 80000 pesos desde "CA-001" hacia "CC-002"
    Entonces la operación es rechazada
    Y veo el mensaje "Saldo insuficiente"
    Y los saldos de mis cuentas no se modifican

  Esquema del escenario: Validación de montos inválidos
    Cuando transfiero <monto> pesos desde "CA-001" hacia "CC-002"
    Entonces la operación es rechazada
    Y veo el mensaje "<mensaje>"

    Ejemplos:
      | monto  | mensaje                             |
      | 0      | El monto debe ser mayor a cero      |
      | -500   | El monto debe ser mayor a cero      |
      | 999999 | Supera el límite diario de 500000   |

Notá tres cosas: el Antecedentes evita repetir el setup, el Esquema del escenario cubre tres casos con una sola plantilla, y los tags (@critico) permiten filtrar qué corre en cada pipeline.

Las step definitions. Gherkin por sí solo no ejecuta nada: necesita el código que conecta cada paso con el sistema. En Python con behave:

Código: python
# features/steps/transferencias.py
from behave import given, when, then
from app.banco import Banco, SaldoInsuficiente, MontoInvalido


@given('que estoy autenticado como "{usuario}"')
def step_autenticar(context, usuario):
    context.banco = Banco()
    context.sesion = context.banco.login(usuario)


@given('tengo una {tipo} "{numero}" con saldo de {saldo:d} pesos')
def step_crear_cuenta(context, tipo, numero, saldo):
    context.banco.abrir_cuenta(context.sesion, numero, tipo, saldo)


@when('transfiero {monto:d} pesos desde "{origen}" hacia "{destino}"')
def step_transferir(context, monto, origen, destino):
    context.error = None
    try:
        context.resultado = context.banco.transferir(
            context.sesion, origen, destino, monto
        )
    except (SaldoInsuficiente, MontoInvalido) as e:
        context.error = e


@then('la operación se confirma')
def step_confirmada(context):
    assert context.error is None, f"Fallo inesperado: {context.error}"
    assert context.resultado.estado == "CONFIRMADA"


@then('la operación es rechazada')
def step_rechazada(context):
    assert context.error is not None, "Se esperaba un rechazo"


@then('el saldo de "{numero}" es {esperado:d} pesos')
def step_verificar_saldo(context, numero, esperado):
    real = context.banco.consultar_saldo(context.sesion, numero)
    assert real == esperado, f"Esperado {esperado}, obtenido {real}"


@then('veo el mensaje "{mensaje}"')
def step_mensaje(context, mensaje):
    assert str(context.error) == mensaje

Lo mismo en JavaScript con Cucumber.js:

Código: javascript
// features/step_definitions/transferencias.js
const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('assert');
const { Banco, SaldoInsuficiente } = require('../../src/banco');

Given('que estoy autenticado como {string}', function (usuario) {
  this.banco = new Banco();
  this.sesion = this.banco.login(usuario);
});

When(
  'transfiero {int} pesos desde {string} hacia {string}',
  function (monto, origen, destino) {
    this.error = null;
    try {
      this.resultado = this.banco.transferir(this.sesion, origen, destino, monto);
    } catch (e) {
      this.error = e;
    }
  }
);

Then('el saldo de {string} es {int} pesos', function (numero, esperado) {
  const real = this.banco.consultarSaldo(this.sesion, numero);
  assert.strictEqual(real, esperado);
});

Y para correrlo, filtrando por tags:

Código: bash
# Python / behave
behave --tags=@critico --lang=es

# JavaScript / Cucumber.js
npx cucumber-js --tags "@critico and not @wip"

# Java / Cucumber-JVM con Maven
mvn test -Dcucumber.filter.tags="@critico"



5. Cómo escribir buen Gherkin

Acá es donde el 90% de los equipos falla. Escribir Gherkin es fácil; escribirlo bien es otra cosa.

Regla 1: declarativo, no imperativo. Este es el error más frecuente. Comparemos.

Mal (imperativo — describe la interfaz, no el comportamiento):

Código: gherkin
Escenario: Login
  Dado que abro el navegador en "https://banco.com/login"
  Cuando escribo "mlopez" en el campo con id "#username"
  Y escribo "secreto123" en el campo con id "#password"
  Y hago clic en el botón ".btn-primary"
  Y espero 3 segundos
  Entonces veo el div con clase ".dashboard-container"

Bien (declarativo — describe la intención):

Código: gherkin
Escenario: Login exitoso con credenciales válidas
  Dado que soy un cliente registrado
  Cuando ingreso con mis credenciales válidas
  Entonces accedo a mi panel de cuentas

¿Por qué importa? Porque si mañana el botón cambia de clase CSS, el primer escenario se rompe aunque el comportamiento del negocio no cambió. El segundo sigue siendo válido durante años. Los selectores, las esperas y los detalles de la UI van en las step definitions, nunca en el archivo .feature.

Regla 2: un solo When por escenario. Si tenés varios When, estás describiendo un flujo, no un comportamiento. Partilo.

Regla 3: nada de pasos conjuntivos. Si un paso dice "Dado que tengo una cuenta y estoy logueado y tengo saldo", separalo en tres Given. La reutilización muere con los pasos conjuntivos.

Regla 4: evitá escenarios encadenados. Cada escenario debe poder correr solo y en cualquier orden. Si el escenario 2 depende de que el 1 haya corrido antes, tenés una bomba de tiempo: el día que uno falle, fallan todos en cascada y no vas a saber cuál es la causa real.

Regla 5: el Then describe algo observable por el negocio. "Entonces el registro en la tabla usuarios tiene estado = 2" no es Gherkin, es una consulta SQL disfrazada. "Entonces la cuenta queda activada" sí lo es.

Regla 6: si nadie de negocio lo va a leer, no uses Gherkin. Esto es importante y poco dicho. Gherkin tiene un costo real: una capa de indirección entre el texto y el código. Ese costo se paga sólo si hay alguien no técnico que efectivamente lee y valida esos escenarios. Si tu equipo es 100% técnico y nadie de Producto abre nunca un .feature, estás pagando el costo sin recibir el beneficio: escribí pruebas de integración normales y listo.



6. Tabla comparativa

AspectoTDDBDDGherkin
Qué esTécnica de diseñoPráctica de colaboraciónLenguaje / sintaxis
ObjetivoConstruir bien la cosaConstruir la cosa correctaExpresar ejemplos
Quién participaDesarrolladorNegocio + Dev + QALos tres
NivelUnidad / componenteComportamiento / featureN/A
LenguajeCódigoNatural + códigoNatural estructurado
Creado porKent Beck (~1999)Dan North (2006)Aslak Hellesøy (Cucumber)
Herramientaspytest, JUnit, JestCucumber, behave, SpecFlowParte de Cucumber
VelocidadMilisegundosSegundos a minutosN/A

Stack por lenguaje:

LenguajeTDDBDD / Gherkin
Pythonpytest, unittestbehave, pytest-bdd
JavaScript / TSJest, VitestCucumber.js, CodeceptJS
JavaJUnit 5, TestNGCucumber-JVM, JBehave
C# / .NETxUnit, NUnitReqnroll (ex SpecFlow)
RubyRSpec, MinitestCucumber
Gotesting, testifygodog
PHPPHPUnitBehat



7. Errores comunes

"Hacemos BDD porque usamos Cucumber". El más extendido. Si los .feature los escribe el automatizador solo, después de que la funcionalidad ya está desarrollada, y nadie de negocio los lee nunca, eso no es BDD: es automatización con sintaxis Gherkin. BDD sin la conversación de Discovery es puro overhead.

Usar Gherkin para pruebas de UI paso a paso. Termina en archivos .feature de 40 líneas llenos de clics y esperas, imposibles de mantener. Para eso usá directamente Playwright o Selenium.

Poner todo en el nivel de BDD. Los escenarios Gherkin son lentos. Van arriba de la pirámide, y son pocos: los flujos críticos de negocio. La validación de que una función redondea bien un decimal va en una prueba unitaria con TDD, no en un .feature.

TDD como "escribir los tests después". Si escribís el código y después los tests, eso es testing, y está bien, pero no es TDD y no te da el beneficio de diseño. El orden es el punto entero de la técnica.

Perseguir el 100% de cobertura. La cobertura mide qué líneas se ejecutaron, no si las verificaste bien. Podés tener 100% de cobertura con cero asserts. Es una métrica útil para encontrar zonas sin tocar, no un objetivo en sí.



Cierre

Resumiendo la cadena completa: BDD arranca con una conversación entre negocio, desarrollo y QA que produce ejemplos concretos. Esos ejemplos se formulan en Gherkin, que es el lenguaje compartido. La automatización de esos escenarios forma el loop externo, y dentro de él el desarrollador aplica TDD para construir cada pieza con buen diseño.

No son alternativas entre las que hay que elegir: son capas que se complementan. Y si tuviera que quedarme con una sola idea para llevarse: el valor de BDD no está en los archivos .feature, está en la conversación que los produce. Si te saltás la conversación, te queda la parte cara y ninguno de los beneficios.

Les dejo las preguntas de siempre:

  • ¿Alguien está haciendo BDD de verdad, con negocio sentado en la mesa de Discovery? ¿Cómo lo lograron?
  • ¿TDD les funcionó o lo abandonaron? Me interesan las dos respuestas, sobre todo la segunda.
  • ¿Gherkin en español o en inglés? Hay opiniones fuertes en ambas direcciones.

Nos leemos.



Créditos de las imágenes: diagramas de Wikimedia Commons — You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login y You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login (CC BY-SA). Los diagramas del doble loop y de los Three Amigos son propios.

Saludos,
ANTRAX