EARS + Gherkin: derivar escenarios desde requisitos

Por: Artiko
gherkinearstrazabilidadspecification-by-example

EARS + Gherkin

EARS y Gherkin operan en capas distintas pero complementarias:

CapaLenguajePregunta que responde
RequisitoEARS¿Qué debe hacer el sistema?
EscenarioGherkin¿Cómo verificamos que lo hace?

EARS define el contrato del sistema; Gherkin define ejemplos ejecutables de ese contrato.

Este capítulo asume familiaridad con EARS. Si no lo conocés, leé el tutorial EARS primero.

Reglas de derivación

flowchart LR
    REQ[Requisito EARS<br/>When/While/Where/If] --> Scen[Escenario Gherkin<br/>Given/When/Then]
    REQ -.->|disparador When/If| ScenWhen[When del Gherkin]
    REQ -.->|sujeto + respuesta| ScenThen[Then del Gherkin]
    REQ -.->|estado/feature While/Where| ScenGiven[Given del Gherkin]

Mapeo:

Ejemplo 1: derivación directa

EARS:

REQ-AUTH-001: When the user submits valid credentials to /auth/login, the auth service shall return HTTP 200 with a JWT valid for 24 hours.

Gherkin:

Feature: Login con credenciales válidas

  @REQ-AUTH-001
  Scenario: Usuario obtiene un JWT al enviar credenciales válidas
    Given un usuario registrado con email "[email protected]" y password "secret123"
    When envío POST /auth/login con email "[email protected]" y password "secret123"
    Then la respuesta tiene status 200
    And el cuerpo contiene un campo "token"
    And el token es un JWT válido
    And el JWT expira en 24 horas

Un REQ → un scenario con tag @REQ-AUTH-001. Trazabilidad directa.

Ejemplo 2: un EARS, varios scenarios (casos de borde)

EARS:

REQ-AUTH-002: If the credentials are invalid, then the auth service shall return HTTP 401 with body { "error": "invalid_credentials" } indistinguishably between "user not found" and "wrong password".

Gherkin (tres scenarios cubren el REQ):

Feature: Manejo de credenciales inválidas

  @REQ-AUTH-002
  Scenario: Email inexistente devuelve 401 sin filtrar info
    Given que no existe un usuario con email "[email protected]"
    When envío POST /auth/login con email "[email protected]" y password "anything"
    Then la respuesta tiene status 401
    And el cuerpo es { "error": "invalid_credentials" }

  @REQ-AUTH-002
  Scenario: Password incorrecto devuelve 401 sin filtrar info
    Given un usuario registrado con email "[email protected]" y password "secret123"
    When envío POST /auth/login con email "[email protected]" y password "wrong-password"
    Then la respuesta tiene status 401
    And el cuerpo es { "error": "invalid_credentials" }

  @REQ-AUTH-002 @security
  Scenario: Respuestas indistinguibles entre usuario inexistente y password incorrecto
    Given que no existe un usuario con email "[email protected]"
    And un usuario registrado con email "[email protected]" y password "secret123"
    When capturo la respuesta de POST /auth/login con "[email protected]" y "anything" como R1
    And capturo la respuesta de POST /auth/login con "[email protected]" y "wrong-password" como R2
    Then R1 y R2 tienen el mismo status
    And R1 y R2 tienen el mismo cuerpo
    And R1 y R2 tienen tiempos de respuesta dentro de 50 ms de diferencia

Tres scenarios cubren la indistinguibilidad declarada en el REQ.

Ejemplo 3: EARS con Where → Scenario Outline

EARS:

REQ-EXPORT-001: Where the user has subscription <plan>, when the user requests an export, the export service shall allow files up to <max_size> MB.

Gherkin (Outline parametrizado):

Feature: Límites de export según plan

  @REQ-EXPORT-001
  Scenario Outline: Límite de export por plan
    Given un usuario con subscripción "<plan>"
    When solicito export de "<size_mb>" MB
    Then la respuesta tiene status <expected_status>

    Examples:
      | plan    | size_mb | expected_status |
      | free    | 10      | 200             |
      | free    | 50      | 413             |
      | pro     | 50      | 200             |
      | pro     | 200     | 413             |
      | premium | 500     | 200             |
      | premium | 2000    | 413             |

Una sola plantilla cubre 6 combinaciones del requisito.

Ejemplo 4: EARS con While → Given de estado

EARS:

REQ-SYNC-005: While the device is offline, when the user submits a form, the application shall persist the submission locally and shall display "Pending sync" in the status bar.

Gherkin:

@REQ-SYNC-005
Scenario: Submit offline persiste localmente y muestra indicador
  Given el dispositivo está offline
  And un formulario abierto con datos válidos
  When el usuario presiona "Enviar"
  Then los datos del formulario están guardados en el storage local
  And el status bar muestra "Pending sync"
  And no se realizó ninguna petición HTTP

While offline se convierte en Given el dispositivo está offline.

Trazabilidad bidireccional

REQ → Scenarios

Buscás un REQ y querés saber qué scenarios lo cubren:

grep -rn "@REQ-AUTH-001" features/

Scenarios → REQ

Buscás un scenario y querés saber qué REQ implementa:

@REQ-AUTH-001
Scenario: Usuario obtiene un JWT
  ...

El tag responde la pregunta.

Reporte de cobertura

Generar mapa requisito → scenarios desde el reporte de Cucumber:

import fs from 'fs'

const report = JSON.parse(fs.readFileSync('reports/cucumber.json', 'utf-8'))
const coverage = new Map<string, string[]>()

for (const feature of report) {
  for (const scenario of feature.elements ?? []) {
    const reqs = (scenario.tags ?? [])
      .map(t => t.name)
      .filter(name => name.startsWith('@REQ-'))
    
    for (const req of reqs) {
      const list = coverage.get(req) ?? []
      list.push(scenario.name)
      coverage.set(req, list)
    }
  }
}

// Imprimir
for (const [req, scenarios] of coverage) {
  console.log(`${req}: ${scenarios.length} scenarios`)
  for (const s of scenarios) console.log(`  - ${s}`)
}

Si un REQ no aparece en el mapa, no tiene cobertura — es señal para escribir más scenarios.

Flujo recomendado

flowchart TD
    A[PO escribe Historia + AC en EARS] --> B[Three Amigos discute AC]
    B --> C[QA deriva Gherkin desde EARS]
    C --> D[Dev implementa código + step defs]
    D --> E[CI ejecuta features]
    E -->|pasa| F[PR merged]
    E -->|falla| G[Volver a EARS o impl]
    F --> H[Living docs actualizada]

Patrón: spec.md con EARS y Gherkin enlazado

Mantené un archivo por capability donde EARS y Gherkin estén lado a lado:

# Spec: Autenticación

## Requisitos (EARS)

REQ-AUTH-001: When the user submits valid credentials to /auth/login, the auth service shall return HTTP 200 with a JWT valid for 24 hours.

REQ-AUTH-002: If the credentials are invalid, then the auth service shall return HTTP 401 with body { "error": "invalid_credentials" }.

REQ-AUTH-003: While the JWT is valid, when a request arrives with the JWT in the Authorization header, the auth middleware shall populate `req.user` with the decoded claims.

REQ-AUTH-004: If the JWT signature is invalid or expired, then the auth middleware shall return HTTP 401.

## Scenarios (features/auth/)

- [`login.feature`](../features/auth/login.feature) — cubre REQ-AUTH-001, REQ-AUTH-002
- [`auth-middleware.feature`](../features/auth/auth-middleware.feature) — cubre REQ-AUTH-003, REQ-AUTH-004

## Implementación

- `src/auth/login.handler.ts` — REQ-AUTH-001, REQ-AUTH-002
- `src/auth/middleware.ts` — REQ-AUTH-003, REQ-AUTH-004

Trazabilidad triple: REQ ↔ feature ↔ código.

Combinación con OpenSpec, Spec-Kit, BMAD

Frameworks de Spec-Driven Development que vimos en otros tutoriales:

En todos ellos, EARS es la capa de requisito y Gherkin es la capa de test.

Antipatrón: Gherkin sin EARS de soporte

Sin EARS, los Gherkin tienden a:

Con EARS, cada scenario tiene un REQ identificable como punto de referencia.

Antipatrón: EARS sin Gherkin de verificación

Sin Gherkin, los EARS son papel. El sistema puede divergir del contrato sin que nadie lo note. Los tests automatizados son el guardián del contrato.

Ejemplo completo: feature de carrito

Spec:

## Requisitos

REQ-CART-001: When the user adds a product to the cart, the cart service shall increment the quantity if the product is already in the cart, or insert it with quantity 1 if not.

REQ-CART-002: When the user removes a product from the cart, the cart service shall remove the line item completely.

REQ-CART-003: While the cart is empty, the checkout button shall be disabled.

REQ-CART-004: If the cart subtotal exceeds $10000, then the cart service shall require manager approval before allowing checkout.

Gherkin:

Feature: Carrito de compras

  Background:
    Given un cliente registrado con email "[email protected]"

  @REQ-CART-001
  Scenario: Agregar producto nuevo al carrito
    Given un carrito vacío
    When agrego el producto "P-100" al carrito
    Then el carrito contiene 1 línea
    And la línea de "P-100" tiene quantity 1

  @REQ-CART-001
  Scenario: Agregar producto existente incrementa quantity
    Given un carrito con la línea (P-100, quantity 2)
    When agrego el producto "P-100" al carrito
    Then el carrito contiene 1 línea
    And la línea de "P-100" tiene quantity 3

  @REQ-CART-002
  Scenario: Remover producto elimina la línea completa
    Given un carrito con la línea (P-100, quantity 5)
    When remuevo el producto "P-100" del carrito
    Then el carrito está vacío

  @REQ-CART-003
  Scenario: Botón checkout deshabilitado con carrito vacío
    Given un carrito vacío
    When abro la vista del carrito
    Then el botón "Checkout" está deshabilitado

  @REQ-CART-004
  Scenario: Carrito sobre el límite requiere aprobación
    Given un carrito con subtotal $15000
    When inicio el checkout
    Then la respuesta indica "requires_manager_approval: true"
    And no se procesa el pago hasta recibir aprobación

Cinco scenarios cubren cuatro REQ. Cobertura 100%, trazabilidad explícita.

Resumen

En el último capítulo cubrimos la integración con CI/CD.