Step Definitions en TypeScript
Step Definitions en TypeScript
Cucumber-JS es el runner BDD para Node.js. Esta sección cubre las step definitions con TypeScript en profundidad.
Setup completo
npm install --save-dev @cucumber/cucumber typescript ts-node @types/node
tsconfig.json mínimo:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
cucumber.cjs:
module.exports = {
default: {
requireModule: ['ts-node/register'],
require: ['features/step_definitions/**/*.ts', 'features/support/**/*.ts'],
paths: ['features/**/*.feature'],
format: ['progress-bar', '@cucumber/pretty-formatter', 'html:reports/cucumber.html'],
formatOptions: { snippetInterface: 'async-await' }
}
}
Estructura recomendada
features/
├── auth/
│ ├── login.feature
│ └── signup.feature
├── cart/
│ ├── add-product.feature
│ └── checkout.feature
├── step_definitions/
│ ├── auth.steps.ts
│ └── cart.steps.ts
└── support/
├── world.ts
├── hooks.ts
└── helpers/
├── api-client.ts
└── db.ts
Las anotaciones: Given, When, Then
import { Given, When, Then } from '@cucumber/cucumber'
Given('una calculadora vacía', function () {
this.calc = new Calculator()
})
When('sumo {int} y {int}', function (a: number, b: number) {
this.calc.add(a, b)
})
Then('el resultado es {int}', function (expected: number) {
expect(this.calc.getResult()).toBe(expected)
})
Given, When y Then son funcionalmente intercambiables — la palabra clave que usás determina la categoría del step, pero Cucumber no hace distinciones técnicas. Por convención usá la palabra correcta.
Cucumber Expressions
El sistema de matching default. Soporta tipos built-in:
| Placeholder | Captura | Tipo TS |
|---|---|---|
{int} | Entero | number |
{float} | Float | number |
{word} | Palabra sin espacios | string |
{string} | Cadena entre "..." o '...' | string |
{} | Cualquier cosa (no recomendado) | string |
When('agrego {int} unidades de {string} al carrito', function (qty: number, sku: string) {
this.cart.add(sku, qty)
})
El paso:
When agrego 3 unidades de "P-100" al carrito
Matchea con qty = 3 y sku = 'P-100'.
Parameter types custom
Definí tipos custom para hacer las step definitions más expresivas y type-safe.
// features/support/parameters.ts
import { defineParameterType } from '@cucumber/cucumber'
defineParameterType({
name: 'product',
regexp: /[A-Z]-\d{3}/,
transformer: (sku) => ({ sku }),
})
defineParameterType({
name: 'currency',
regexp: /\$\d+(\.\d{2})?/,
transformer: (s) => Number(s.replace('$', '')),
})
defineParameterType({
name: 'plan',
regexp: /free|pro|premium/,
transformer: (name) => name as 'free' | 'pro' | 'premium',
})
Uso:
When('agrego {product} al carrito', function (product: { sku: string }) {
this.cart.add(product.sku)
})
Then('el subtotal es {currency}', function (expected: number) {
expect(this.cart.subtotal).toBe(expected)
})
Given('un usuario con plan {plan}', function (plan: 'free' | 'pro' | 'premium') {
this.user = { plan }
})
Los .feature quedan limpios:
Given un usuario con plan pro
When agrego P-100 al carrito
Then el subtotal es $25.00
RegExp en lugar de Cucumber Expressions
Si necesitás más control:
import { When } from '@cucumber/cucumber'
When(/^agrego (\d+) unidades de "([^"]+)" al carrito$/, function (qty: string, sku: string) {
this.cart.add(sku, parseInt(qty, 10))
})
Más poderoso pero menos legible. Usá Cucumber Expressions por default.
El World — estado compartido entre steps
this dentro de un step es el World, instancia que se crea fresh por scenario. Es el lugar para compartir estado.
// features/support/world.ts
import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber'
import { ApiClient } from './helpers/api-client'
export interface CartItem {
sku: string
qty: number
}
export class CustomWorld extends World {
api: ApiClient
cart: CartItem[] = []
response?: { status: number; body: any }
user?: { id: string; plan: 'free' | 'pro' | 'premium' }
constructor(options: IWorldOptions) {
super(options)
this.api = new ApiClient(process.env.API_URL ?? 'http://localhost:3000')
}
}
setWorldConstructor(CustomWorld)
Y en las step definitions:
import { Given, When, Then } from '@cucumber/cucumber'
import { CustomWorld } from '../support/world'
Given('un usuario con plan {plan}', function (this: CustomWorld, plan) {
this.user = { id: 'U-001', plan }
})
When('agrego {product} al carrito', function (this: CustomWorld, product) {
this.cart.push({ sku: product.sku, qty: 1 })
})
Then('el carrito tiene {int} líneas', function (this: CustomWorld, count: number) {
expect(this.cart.length).toBe(count)
})
Tipar this te da autocompletado y type-checking.
Hooks
import { Before, After, BeforeAll, AfterAll, BeforeStep, AfterStep } from '@cucumber/cucumber'
BeforeAll(async () => {
await startTestDatabase()
})
AfterAll(async () => {
await stopTestDatabase()
})
Before(async function (this: CustomWorld) {
await truncateTables()
})
After(async function (this: CustomWorld, scenario) {
if (scenario.result?.status === 'FAILED') {
console.log('Scenario falló:', scenario.pickle.name)
// ej. capturar screenshot, dump de logs
}
})
Before({ tags: '@authenticated' }, async function (this: CustomWorld) {
const { token } = await this.api.post('/auth/login', { email: '[email protected]', password: 'test' })
this.api.setAuthToken(token)
})
Orden de ejecución:
sequenceDiagram
BeforeAll->>Before: Inicio
Before->>BeforeStep: Antes de cada step
BeforeStep->>Step: ejecutar
Step->>AfterStep: Después de cada step
AfterStep->>After: Fin del scenario
After->>AfterAll: Fin de la suite
Async/await
Los steps async se manejan con async function:
When('envío POST {string} con body:', async function (this: CustomWorld, path: string, body: string) {
this.response = await this.api.post(path, JSON.parse(body))
})
Then('la respuesta tiene status {int}', function (this: CustomWorld, expected: number) {
expect(this.response?.status).toBe(expected)
})
Cucumber espera a que la Promise se resuelva antes de pasar al siguiente step.
Manejo de errores
Si un step lanza, el scenario falla con ese error.
Then('la respuesta tiene status {int}', function (this: CustomWorld, expected: number) {
if (this.response?.status !== expected) {
throw new Error(`Esperaba status ${expected}, fue ${this.response?.status}`)
}
})
Usá una librería de aserciones: node:assert, chai, vitest/expect, jest. La mayoría tienen mensajes informativos out of the box.
Steps que devuelven ‘pending’
Útil cuando un step todavía no está implementado:
When('hago algo todavía no implementado', function () {
return 'pending'
})
El scenario aparece como pending en el reporte, no como failed. Útil durante TDD outside-in.
Steps que se saltean
When('un step que no debería ejecutarse', function () {
return 'skipped'
})
Anti-patrones en step definitions
1. Steps demasiado específicos
- Given('un usuario con email [email protected] y password Secret123 y rol admin', () => { ... })
+ Given('un usuario con email {string}, password {string} y rol {string}', (email, password, role) => { ... })
Generalizá con parámetros.
2. Pegado de strings frágil
- When(/^hago clic en "(.+)" y espero (\d+) segundos$/, ...) // matchea cualquier cosa
+ When('hago clic en {string} y espero {int} segundos', ...)
Cucumber Expressions son menos frágiles que regex.
3. Step definitions con lógica de negocio
When('proceso el pago', function () {
- // lógica completa del pago acá
- if (this.cart.total > 100) ...
- if (this.user.plan === 'premium') ...
+ this.response = await this.api.post('/payments', { cart: this.cart, user: this.user })
})
Las step definitions son wrappers thin. La lógica vive en la aplicación, no en los tests.
4. Estado en variables module-scope
- let currentUser // ⚠ módulo-scope, contamina entre scenarios
+ // Usar this.user en el World
Snippets autogenerados
Cuando ejecutás un scenario con steps no implementados, Cucumber imprime el código snippet:
? When agrego "P-100" al carrito
Implement with the following snippet:
When('agrego {string} al carrito', function (string) {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
Copialo, completalo, y el step queda listo.
Resumen
Given,When,Thendefinen steps (intercambiables técnicamente, distintos semánticamente)- Cucumber Expressions:
{int},{string}, custom{product} Worldpara estado compartido (instancia por scenario)- Hooks:
Before,After,BeforeAll,AfterAll, con tags - Async/await soportado nativamente
- Step definitions = wrappers thin, no lógica de negocio
En el siguiente capítulo cubrimos el equivalente en Python con behave.