Migrando a nukadoko
Para qué sirve
Migrar una suite existente a nukadoko, una pieza a la vez, sin reescribirla. El punto de partida varía:
- Una suite de cucumber-js — archivos feature más pegamento (glue), normalmente usando Playwright.
- Una suite ya estructurada como pasos tipados — su propio DSL, no el de cucumber,
pero lo suficientemente parecido a
defineStepde nukadoko como para que la migración sea principalmente traducción.
En cualquier caso, la migración ocurre en etapas, nunca de golpe; el resto de esta habilidad explica por qué y luego recorre cada punto de partida por turno.
Todo lo siguiente asume que el proyecto ya está inicializado. Si no lo está aún
(sin nukadoko.config.ts), ejecuta nuka init primero.
Dos etapas, nunca a la vez
Cambia una cosa y luego la siguiente, nunca ambas a la vez. Si un paso se rompe después de que dos cambios lleguen juntos, no hay forma de saber cuál lo rompió: las causas están enredadas en un solo fallo. Dividir el trabajo en etapas mantiene cada fallo trazable hasta exactamente un cambio. Ese es el objetivo principal — las etapas en sí mismas son secundarias.
Si las etapas a continuación no encajan con tu punto de partida, deriva tus propias dos etapas a partir de esto. Lo que debe cumplirse es la trazabilidad, no estos nombres particulares de etapas.
Dos aplicaciones siguen: una suite de cucumber-js se divide en "cambiar la importación" y "añadir tipado" (abajo); una suite ya estructurada como pasos tipados se divide de forma diferente, descrita más adelante.
Viniendo de una suite de cucumber-js
Este es el caso al que se refieren las dos etapas anteriores.
Etapa 1 — haz que funcione
Cambia la importación que usa cada archivo de pegamento:
// antes: import { Given, When, Then } from "@cucumber/cucumber";
import { Given, When, Then } from "nukadoko/compat";
La extensión determina lo que descubre el sistema de lectura, no el historial de la suite:
los archivos .ts, .mts, .js y .mjs se leen de la misma manera, por lo que una suite
que siempre fue JavaScript plano no necesita reescribirse a TypeScript solo para empezar
esta etapa. Un archivo .cjs es la única excepción: nukadoko es solo ESM, por lo que el
descubrimiento nunca lo importa; nuka check lo nombra en su lugar
(step-file-unsupported-extension) en lugar de dejarlo desaparecer como un undefined-step
no explicado. Renombrarlo a .js solo ayuda si el código del archivo ya es sintaxis de
módulo ES; si todavía usa require(...), fallará en la misma importación por la misma
razón de ESM, y nuka check lo indicará como step-file-import-failed.
Luego ejecuta nuka check <feature> y nuka run <feature> y lee lo que dicen.
Arregla lo que señalen y ejecútalos de nuevo. Repite hasta que la suite existente
esté en verde — esa es la condición de finalización de la Etapa 1, nada más.
Esa condición es que nuka check y nuka run estén en verde, no que tsc
verifique tipos sin errores; los dos pueden diferir en cualquier dirección.
nuka steps y nuka describe siguen siendo utilizables durante toda la Etapa 1,
incluso mientras algunos archivos de pegamento aún no se importan correctamente:
ambos leen archivos de pasos uno a la vez, por lo que un archivo que aún no se importa
se nombra (import_failures en --json, stderr en caso contrario) en lugar de vaciar
todo el vocabulario. Lee el vocabulario sobre la marcha en lugar de esperar a que todos
los archivos se importen limpios primero; nuka run <feature> y nuka do <step> aún se
niegan rotundamente ante un archivo de pegamento ilegible, a propósito, ya que están a
punto de ejecutarse.
No busques una lista de lo que compat no soporta antes de empezar. Cualquier cosa que no
funcione fallará ruidosamente, ya sea en la importación o en el primer nuka run — el
fallo nombra lo que se rompió, y eso es lo que hay que arreglar. Una lista escrita aquí
quedaría obsoleta en cuanto la cobertura de compat crezca; la salida del CLI nunca lo hace.
Etapa 2 — dale contratos
Una vez que la suite funciona, algunos pasos merecen ser tipados. Esto es lo que cambia cuando uno lo es:
| paso compat | paso tipado | |
|---|---|---|
| Entrada | solo captura de patrón, sin verificar | validada contra un esquema args, cada campo con su .describe() |
| Salida | descartada — el result del recibo es null | validada contra un esquema returns y almacenada en el recibo |
| Dependencias | efectos secundarios en el World, invisibles en la firma de la función | declaradas con from, nombradas en una importación, verificadas por nuka check antes de ejecutar nada, y registradas como used en el recibo |
| Palabra clave | decorativa — un paso vinculado a Then aún puede mutar | mutates es una declaración en la que nukadoko confía: declarar mutates: true y un entorno de solo lectura se niega a ejecutarlo, y nuka check advierte si está vinculado a Then; lo que realmente se ejecutó se registra en los contadores observed del recibo |
| Ejecución aislada | no posible (el World está vacío fuera de un escenario) | nuka do <step> lo ejecuta directamente, recibo impreso en stdout; una clave from viene de --args como cualquier otra, o de --use <receipt-id> para una obtenida de una ejecución anterior; una llamada a fixture resultOf dentro de run aún no encuentra nada, ya que no hay escenario para recorrer |
Esa última fila es un hecho separado de la fila "Dependencias" anterior, no una
consecuencia de ella: un paso compat no puede ejecutarse solo porque lo que necesita
vive en un World que nadie ha poblado aún, y el World no es parte de su firma — no hay
nada que inspeccionar para saber qué configurar primero. Las dependencias de un paso
tipado se nombran como entradas from, visibles en una importación, por lo que una
llamada nuka do puede proporcionarlas manualmente: --args para una clave normal,
o --use <receipt-id> para una obtenida del resultado de una ejecución anterior — el
nombre del paso ascendente nunca tiene que aparecer en la línea de comandos, porque el
recibo citado ya lo lleva. Un paso cuyas claves lleguen todas de esa manera no necesita
--args en absoluto: --use por sí solo es una invocación completa. Lo que aún no
encuentra nada fuera de un escenario es una dependencia leída a través de la fixture
resultOf desde dentro de run: esa llamada no tiene cadena que recorrer cuando no
hay escenario para construir una, con --use o sin él.
Promociona primero los pasos que ejecutas con más frecuencia, uno a la vez — no toda la suite de una sola vez. Cómo reescribir cualquier pieza de pegamento es una decisión que se toma en el momento; nukadoko no prescribe una receta única, y esta habilidad tampoco lo hará.
Al redactar el run de un paso tipado, a veces se aterriza en la forma no migrada
run(ctx, args) antes de desestructurarla completamente. nuka steps --json ya nombra
qué fixtures toca ese borrador, como needs_inferred: una suposición léxica leída de
los accesos a miembros de ctx, junto con needs: null y un needs_error que explica
por qué el contrato real aún no puede leerse. Ahorra tener que releer todo el cuerpo
manualmente para saber qué añadir a la firma desestructurada, pero es una suposición,
no el contrato: se pierde un alias (const c = ctx) y nunca nombra needs_browser,
así que termina de desestructurar en lugar de tratarlo como terminado.
Cómo se ve un paso promocionado
Un ejemplo. Antes, pegamento compat:
const { Given } = require("@cucumber/cucumber");
Given("un proyecto {string} existe", async function (name) {
const res = await this.request.post("/projects", { data: { name } });
this.projectId = (await res.json()).id;
});
Después, un paso tipado (features/steps/create-project.ts):
import { defineStep } from "nukadoko";
import { z } from "zod";
export default defineStep({
pattern: "un proyecto {name:string} existe",
description: "Crear un proyecto y devolver su id",
args: z.object({
name: z.string().describe("el nombre visible del proyecto"),
}),
returns: z.object({
id: z.string().describe("el id del proyecto creado, para que pasos posteriores lo citen"),
}),
mutates: true,
async run({ request }, args) {
const res = await request.post("/projects", { data: args });
return res.json();
},
});
Lo que cambió:
this.projectIddesaparece; el id vuelve a través dereturns. Un paso posterior declarafrom: { projectId: [createProjectStep, "id"] }para leerlo por clave —nuka checkverifica el orden de vinculación antes de ejecutar nada, y la lectura aparece en el recibo de ese paso posterior (la fixtureresultOfsigue disponible para la lectura más rara que un nombre de clave no puede expresar).- El argumento es una captura nombrada vinculada a una clave del esquema
(
{name:string}→args.name), por lo que el patrón solo muestra qué texto se convierte en qué campo. mutates: trueahora es una declaración, no solo un hecho sobre lo que el código hace —nuka checkadvierte si este paso se vincula alguna vez aThen, y un entorno de solo lectura se niega a ejecutarlo por completo. El recibo aún registra lo que la ejecución realmente envió, para revisión, pero ese recuento no hace que el paso sea rechazado.this.requestse convierte en la fixturerequest, nombrada al desestructurar el primer argumento: solo lo que el ejecutor realmente inyecta puede nombrarse allí, nada implícito, y solo los nombres realmente nombrados se construyen. Un paso que no nombra nipagenicontextnunca inicia un navegador.
Este es el único ejemplo desarrollado aquí. No es un catálogo de todas las brechas entre compat y tipado — si otro patrón surge con suficiente frecuencia como para merecer uno, esa es una adición separada, no algo que improvisar a partir de este único caso.
Cuando un hook global After era la limpieza
Una suite de cucumber-js a menudo se apoya en un hook global After para limpiar
lo que un paso creó: un inquilino, una fila sembrada, un archivo temporal. Vale la
pena revisarlo una vez que el paso que lo creó se promociona: declara el recurso como
una fixture bajo fixtures en nukadoko.config.ts propiamente, con la limpieza
escrita justo después de await use(...) en la misma función que lo construyó, en
lugar de dejarlo en un hook que tiene que adivinar, solo a partir de etiquetas, qué
escenarios realmente lo necesitan. defineFixtures (del paquete nukadoko) mantiene
la fixture completamente tipada; cualquier paso tipado la alcanza de la misma manera
que alcanza page o request, desestructurando el nombre. Esto es aditivo, no
obligatorio: un hook After que limpia algo que ningún paso tipado ha reclamado aún
sigue siendo exactamente tan válido como lo era en la Etapa 1.
Viniendo de un DSL con forma de pasos tipados
Si no hay archivos feature ni pegamento cucumber, la puerta compat de la Etapa 1 anterior no es relevante — salta directamente.
Lo que hace posible esto es que pattern es opcional en defineStep: un paso
puede definirse sin patrón alguno y seguir siendo una pieza completa de vocabulario
solo para CLI, ejecutable con nuka do e inspeccionable con nuka describe. Es un
par diferente de etapas producido por el mismo principio anterior: mueve cada paso a
un defineStep tipado primero, y agrúpalo en un pattern de Gherkin más tarde,
cuando un archivo feature haga que valga la pena.
Si el DSL fuente ya lleva algo como una description, args, returns, mutates
y una función run, la traducción a defineStep es directa — cada uno tiene un
contraparte en defineStep para recibirlo.
Lo que no debes hacer
- No hagas la Etapa 1 y la Etapa 2 a la vez. Mezclar un cambio de importación con tipado añadido en el mismo cambio es exactamente lo que hace que un fallo sea no atribuible — consulta "Dos etapas, nunca a la vez" arriba.
- No dejes que la suite deje de funcionar mientras está parcialmente migrada. Los pasos compat y tipados coexisten en el mismo archivo feature; una suite con algunos pasos promocionados y otros aún compat debe seguir pasando durante todo el proceso. Nunca hagas de "completamente tipado" un requisito previo para "funciona".
- No elimines el pegamento compat antes de que su reemplazo tipado esté escrito
y pasando. Escribe el nuevo paso, hazlo funcionar con
nuka do, solo entonces elimina el viejo — nunca en el orden inverso. - No adivines una solución que
nuka checkonuka runno pidieron. Su salida es la evidencia de lo que está mal; cambiar algo que no señalaron es un cambio basado en una corazonada, no en lo que realmente se rompió.