Tus especificaciones Gherkin te están mintiendo.

No intencionalmente. Empezaron siendo fieles. Pero seis sprints después, alguien refactorizó el flujo de checkout y olvidó actualizar el paso When the user submits payment. El archivo .feature sigue pasando, porque la step definition todavía existe. Simplemente llama a código que ya no coincide con lo que el escenario describe realmente. Tienes tests verdes y confianza falsa. Esta es la trayectoria por defecto del BDD a menos que luches activamente contra ella.

El problema no es que los desarrolladores sean perezosos. Es que la relación entre los archivos .feature y las step definitions es fundamentalmente laxa. Los escenarios Gherkin son strings. Las step definitions son regexes o anotaciones que hacen match con esos strings. No hay un compiler que exija que un cambio en el escenario requiera un cambio correspondiente en el código, o viceversa. La toolchain asume que mantendrás ambos alineados manualmente. No lo harás.

Por qué la disciplina manual falla a escala

Cada equipo empieza con el mismo plan: escribir la spec, implementar los pasos, actualizar ambos juntos. Esto funciona en la semana uno.

Se rompe durante la refactorización. Renombras un concepto de dominio en el código, pero el Gherkin sigue usando la terminología antigua porque cambiarlo significa actualizar doce archivos feature y revisarlos de nuevo con producto. O extraes una nueva regla de validación, pero el escenario existente dependía implícitamente del comportamiento anterior, y nadie se dio cuenta porque la step definition fue silenciosamente generalizada para mantener el test pasando. Las specs se convierten en un universo paralelo, cada vez más inexacto.

El costo no es solo documentación desactualizada. Es confianza. Una vez que los desarrolladores dejan de creer que los archivos feature describen la realidad, dejan de leerlos. Luego dejan de escribirlos. Y entonces vuelves a tener unit tests con nombres opacos y sin un lenguaje compartido con los stakeholders.

Qué significa realmente “estar en sync”

Mantener las specs en sync no se trata de hacer que los tests pasen. Que pasen es fácil. En sync significa tres cosas:

  1. Cada paso Gherkin tiene una step definition correspondiente que hace lo que la spec dice.
  2. Cada step definition es realmente alcanzada por al menos un escenario.
  3. El lenguaje en la spec coincide con el lenguaje en la codebase.

La mayoría de los equipos solo verifican el primer punto, y lo hacen en runtime. Necesitas verificar los tres, y necesitas hacerlo en CI antes de que el código se mergee.

Validación automatizada de pasos con binding estricto

El loose string matching en herramientas como Cucumber es la causa raíz. Puedes endurecerlo haciendo que las step definitions sean referencias de primera clase que el build pueda validar.

En proyectos TypeScript o JavaScript, puedes reemplazar las step definitions basadas en regex con un step registry generado que mapee los pasos Gherkin a referencias de funciones reales. La clave es que el mapeo es generado, no escrito a mano, así que el build falla si un escenario referencia un paso que no existe.

Aquí hay una configuración mínima usando un parser custom y un registry generado. Primero, parsea tus archivos .feature en build time:

// scripts/validate-steps.ts
import { readFileSync, readdirSync } from 'fs';
import { parse } from '@cucumber/gherkin';
import { IdGenerator } from '@cucumber/messages';

const featureFiles = readdirSync('./features').filter(f => f.endsWith('.feature'));
const allSteps = new Set<string>();

for (const file of featureFiles) {
  const content = readFileSync(`./features/${file}`, 'utf-8');
  const gherkinDocument = parse(content, new IdGenerator());
  
  for (const feature of gherkinDocument.feature?.children || []) {
    for (const step of feature.scenario?.steps || []) {
      allSteps.add(step.text);
    }
  }
}

// Import the actual step registry from your test code
import { stepRegistry } from '../steps/registry';

const registeredSteps = new Set(Object.keys(stepRegistry));
const undefinedSteps = [...allSteps].filter(s => !registeredSteps.has(s));
const orphanedSteps = [...registeredSteps].filter(s => !allSteps.has(s));

if (undefinedSteps.length > 0) {
  console.error('Undefined steps:', undefinedSteps);
  process.exit(1);
}

if (orphanedSteps.length > 0) {
  console.error('Orphaned steps:', orphanedSteps);
  process.exit(1);
}

console.log(`Validated ${allSteps.size} steps against ${registeredSteps.size} definitions.`);

Tu step registry expone funciones por su texto Gherkin exacto:

// steps/registry.ts
import { given, when, then } from './step-helpers';

export const stepRegistry: Record<string, Function> = {
  'the user is logged in': given.theUserIsLoggedIn,
  'the user adds an item to the cart': when.theUserAddsAnItemToTheCart,
  'the total should be {int}': then.theTotalShouldBe,
};

Los objetos given, when y then son modules planos con funciones. No hay magia de regex. Si un desarrollador cambia el texto Gherkin, debe agregar una entrada correspondiente al registry, o el build falla. Si elimina un escenario, la detección de pasos huérfanos atrapa la definición sobrante.

Intégralo en CI antes del merge

Un script que los desarrolladores ejecutan localmente es un script que los desarrolladores olvidan ejecutar. Necesitas que la validación haga fallar el build.

Agregalo a tu pipeline de tests:

# .github/workflows/ci.yml
jobs:
  validate-specs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx ts-node scripts/validate-steps.ts
      - run: npm test

El detalle importante es que validate-steps.ts se ejecuta antes del test suite real. Si hay un mismatch entre los archivos feature y las step definitions, querés fallar fast con un error claro, no ejecutar cien escenarios de Cucumber que podrían pasar silenciosamente sobre lógica stale.

Living documentation requiere reportes generados

La validación mantiene la sintaxis alineada, pero no garantiza que las specs sean legibles o útiles. Para eso, necesitás una pipeline de living documentation que genere reportes HTML desde tus archivos feature y los publique en cada merge a main.

Herramientas como Cucumber Reports o Pickles pueden convertir tus archivos .feature en documentación navegable. La clave es que la documentación se genera a partir de los mismos archivos que CI valida. Si un escenario se elimina, desaparece de la documentación. Si el lenguaje cambia, la documentación se actualiza automáticamente. No hay una segunda fuente de verdad que mantener.

Publicá el reporte como artifact en CI, o deployalo a un sitio estático:

# .github/workflows/docs.yml
jobs:
  publish-docs:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @picklesdoc/pickles
      - run: pickles --feature-directory=./features --output-directory=./docs
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./docs

Los stakeholders no necesitan leer Gherkin crudo. Necesitan una página legible en la que confíen que está actualizada. La automatización construye esa confianza.

El trade-off: estrictismo versus expresividad

El enfoque de registry tiene un costo. Perdés la flexibilidad de los patrones regex como /^the user adds (\d+) items? to the cart$/. Cada variante se convierte en una entrada explícita, o en un paso parametrizado con placeholders tipados. Esto es verboso.

La alternativa es mantener los regexes pero agregar un linter más estricto que advierta cuando un patrón es demasiado amplio o cuando un texto de paso no hace match con ningún patrón conocido. Podés obtener el 80% de la seguridad con el 20% de la verbosidad usando los flags built-in dry-run y publish de Cucumber, combinados con un linter custom que verifique step definitions sin uso.

# Dry-run parses all features without executing them, surfacing undefined steps
npx cucumber-js --dry-run

Esto es menos estricto que el enfoque de registry. Atrapa pasos undefined, pero no huérfanos, y no exige alineación semántica. Para equipos con suites existentes grandes, es un punto de partida pragmático. Para proyectos nuevos, el enfoque de registry se paga solo dentro de un mes.

Lo que probamos y no funcionó

Experimentamos con generar Gherkin a partir de comentarios en el código. La idea era que los desarrolladores anotarían sus métodos de test, y una herramienta produciría los archivos .feature. Falló porque el Gherkin está diseñado para ser legible por no-desarrolladores. La prosa generada a partir de nombres de métodos no es legible. Ni siquiera es prosa.

También intentamos imponer pair programming para cada cambio de spec. Ayudó, pero no escaló. El problema es mecánico, y la solución también debería serlo.

Empezá con la detección de pasos undefined hoy

Si tenés una suite de Cucumber existente, el cambio más pequeño y útil es agregar --dry-run a tu pipeline de CI. Toma cinco minutos y atrapará el drift más común: un escenario refactorizado que ya no hace match con ninguna step definition.

Si estás empezando de cero, considerá un enfoque basado en registry. El costo inicial de los mapeos explícitos se recupera con las garantías en build time y la confianza para refactorizar libremente sin preocuparte de que tus specs se estén volviendo stale en silencio.

Tus especificaciones Gherkin deberían describir lo que hace el sistema. Si no podés confiar en que hagan eso, son solo comentarios caros. Automatizá las verificaciones que las mantengan honestas, o aceptá que te van a mentir.