EARS + Gherkin: derivar escenarios desde requisitos
EARS + Gherkin
EARS y Gherkin operan en capas distintas pero complementarias:
| Capa | Lenguaje | Pregunta que responde |
|---|---|---|
| Requisito | EARS | ¿Qué debe hacer el sistema? |
| Escenario | Gherkin | ¿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:
- EARS
Where <feature>→ GherkinGiven <feature está habilitada> - EARS
While <state>→ GherkinGiven el sistema en <estado> - EARS
When <trigger>→ GherkinWhen <acción> - EARS
If <condición>→ GherkinWhen <condición ocurre>(en scenario de error) - EARS “shall
” → Gherkin Then <verificación>
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:
- OpenSpec: define
proposal+specs+tasks. Los specs pueden estar en EARS, los tasks pueden referenciar features Gherkin. - Spec-Kit (GitHub): define specs ejecutables. Compatible con tagging de tests.
- BMAD: pipeline de specs y tests. Compatible con Gherkin como output del story-to-test.
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:
- Repetir comportamientos en distintos features
- Faltar cobertura de casos importantes
- Tener inconsistencia entre autores
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
- EARS = contrato. Gherkin = ejemplos ejecutables del contrato.
- Mapeo:
Where/While→Given;When/If→When; “shall” →Then - Trazabilidad vía tags
@REQ-XXX-NNN - Un REQ puede generar varios scenarios (especialmente con
Where→ Outline) - Spec.md como bridge entre EARS y features
En el último capítulo cubrimos la integración con CI/CD.