Migration zu nukadoko
Wofür das gedacht ist
Eine bestehende Suite wird Stück für Stück auf nukadoko umgestellt – kein Neuschreiben. Der Ausgangspunkt ist unterschiedlich:
- Eine cucumber-js-Suite – Feature-Dateien plus Kleber (Glue), die normalerweise Playwright steuert.
- Eine Suite, die bereits als typisierte Schritte strukturiert ist – ein eigenes DSL, nicht das von Cucumber, aber nah genug an nukadokos
defineStep, dass die Umstellung hauptsächlich eine Übersetzung ist.
In beiden Fällen erfolgt die Umstellung in Phasen, niemals auf einmal; der Rest dieses Skills erklärt warum und geht dann auf jeden Ausgangspunkt der Reihe nach ein.
Alles untenstehende setzt voraus, dass das Projekt bereits initialisiert ist. Falls noch nicht (keine nukadoko.config.ts), führen Sie zuerst nuka init aus.
Zwei Phasen, niemals auf einmal
Ändern Sie eine Sache, dann die nächste – niemals beides gleichzeitig. Wenn ein Schritt nach zwei gleichzeitigen Änderungen bricht, gibt es keine Möglichkeit zu sagen, welche Änderung ihn gebrochen hat: Die Ursachen sind in einem einzigen Fehler verwickelt. Die Aufteilung der Arbeit in Phasen hält jeden Fehler auf genau eine Änderung zurückführbar. Das ist der ganze Punkt – die Phasen selbst sind dem untergeordnet.
Wenn die untenstehenden Phasen nicht zu Ihrem Ausgangspunkt passen, leiten Sie Ihre eigenen zwei Phasen daraus ab. Entscheidend ist die Rückverfolgbarkeit, nicht diese speziellen Phasennamen.
Zwei Anwendungen folgen: Eine cucumber-js-Suite teilt sich auf in „Import umstellen“ und „Typisierung hinzufügen“ (unten); eine bereits als typisierte Schritte strukturierte Suite teilt sich anders auf, weiter unten beschrieben.
Ausgehend von einer cucumber-js-Suite
Dies ist der Fall, nach dem die beiden obigen Phasen benannt sind.
Phase 1 – Lauffähig machen
Ändern Sie den einen Import, den jede Glue-Datei verwendet:
// vorher: import { Given, When, Then } from "@cucumber/cucumber";
import { Given, When, Then } from "nukadoko/compat";
Die Erweiterung bestimmt, was die Erkennung liest, nicht die eigene Historie der Suite: .ts, .mts, .js, und .mjs-Glue werden alle gleich gelesen, sodass eine Suite, die immer in einfachem JavaScript war, keinen Umschrieb zu TypeScript benötigt, um diese Phase zu beginnen. Eine .cjs-Datei ist die eine Ausnahme: nukadoko ist nur ESM, daher importiert die Erkennung niemals eine; nuka check nennt sie stattdessen (step-file-unsupported-extension), anstatt sie als unerklärten undefined-step verschwinden zu lassen. Eine Umbenennung in .js hilft nur, wenn der Code der Datei selbst bereits ES-Modul-Syntax ist; wenn er immer noch require(...) aufruft, schlägt dies am selben Import aus demselben ESM-Grund fehl, und nuka check meldet dies als step-file-import-failed.
Dann führen Sie nuka check <feature> und nuka run <feature> aus und lesen Sie, was sie sagen. Beheben Sie, worauf sie hinweisen, und führen Sie sie erneut aus. Wiederholen Sie dies, bis die bestehende Suite grün ist – das ist die Abschlussbedingung von Phase 1, nichts weiter. Diese Bedingung ist, dass nuka check und nuka run grün werden, nicht dass tsc sauber typisiert; die beiden können in beide Richtungen widersprüchlich sein.
nuka steps und nuka describe bleiben während der gesamten Phase 1 nutzbar, selbst wenn einige Glue-Dateien noch nicht importiert werden können: Beide lesen Schrittdateien einzeln, sodass eine Datei, die noch nicht importiert werden kann, benannt wird (import_failures bei --json, sonst stderr), anstatt das gesamte Vokabular zu leeren. Lesen Sie das Vokabular während der Arbeit, anstatt zu warten, bis jede Datei sauber importiert wird; nuka run <feature> und nuka do <step> verweigern die Ausführung bei einer unlesbaren Glue-Datei weiterhin direkt, absichtlich, da sie ausgeführt werden sollen.
Suchen Sie nicht nach einer Liste dessen, was compat nicht unterstützt, bevor Sie beginnen. Was auch immer nicht funktioniert, scheitert lautstark, entweder beim Import oder beim ersten nuka run – der Fehler nennt, was gebrochen ist, und das ist, worauf Sie reagieren. Eine hier geschriebene Liste würde veralten, sobald die Abdeckung von compat wächst; die Ausgabe des CLI selbst tut das nie.
Phase 2 – Verträge vergeben
Sobald die Suite läuft, lohnt es sich, einige Schritte zu typisieren. Dies ändert sich, wenn einer typisiert wird:
| compat-Schritt | typisierter Schritt | |
|---|---|---|
| Eingabe | Nur Muster-Erfassung, ungeprüft | Validiert gegen ein args-Schema, jedes Feld trägt ein .describe() |
| Ausgabe | Verworfen – das Receipts result ist null | Validiert gegen ein returns-Schema und im Receipt gespeichert |
| Abhängigkeiten | Seiteneffekte auf dem World, unsichtbar in der Funktionssignatur | Deklariert mit from, benannt in einem Import, von nuka check geprüft, bevor etwas läuft, und als used im Receipt aufgezeichnet |
| Schlüsselwort | Dekorativ – ein an Then gebundener Schritt kann trotzdem mutieren | mutates ist eine Deklaration, der nukadoko vertraut: mutates: true deklarieren und eine schreibgeschützte Umgebung weigert sich, ihn auszuführen, und nuka check warnt, wenn er an Then gebunden ist; was tatsächlich lief, wird trotzdem in den observed-Zählungen des Receipts aufgezeichnet |
| Allein ausführen | Nicht möglich (das World ist außerhalb eines Szenarios leer) | nuka do <step> führt ihn direkt aus, Receipt wird auf stdout ausgegeben; ein from-Schlüssel kommt von --args wie jeder andere, oder von --use <receipt-id> für einen aus einer früheren Ausführung bezogenen; ein resultOf-Fixture-Aufruf innerhalb von run findet trotzdem nichts, da es kein Szenario gibt, durch das er gehen kann |
Die letzte Zeile ist eine separate Tatsache von der Zeile „Abhängigkeiten“ darüber, keine Folge davon: Ein compat-Schritt kann nicht allein ausgeführt werden, weil das, was er benötigt, auf einem World lebt, das noch niemand bevölkert hat, und das World ist nicht Teil seiner Signatur – es gibt nichts zu inspizieren, um zu wissen, was zuerst eingerichtet werden muss. Die Abhängigkeiten eines typisierten Schritts werden als from-Einträge benannt, in einem Import sichtbar, sodass ein nuka do-Aufruf sie von Hand bereitstellen kann: --args für einen normalen Schlüssel, oder --use <receipt-id> für einen aus dem Ergebnis einer früheren Ausführung – der Name des vorgelagerten Schritts muss nie in der Befehlszeile erscheinen, da das zitierte Receipt ihn bereits trägt. Ein Schritt, bei dem jeder Schlüssel auf diese Weise ankommt, benötigt überhaupt kein --args: --use allein ist ein vollständiger Aufruf. Was außerhalb eines Szenarios immer noch nichts findet, ist eine über das resultOf-Fixture von innerhalb von run gelesene Abhängigkeit: Dieser Aufruf hat keine Kette, die er durchlaufen kann, wenn es kein Szenario gab, um eine zu bauen, unabhängig von --use.
Fördern Sie zuerst die Schritte, die Sie am häufigsten ausführen, einen nach dem anderen – nicht die gesamte Suite in einem Durchgang. Wie ein bestimmtes Stück Glue umgeschrieben wird, ist eine Ermessensentscheidung im Moment; nukadoko schreibt kein Rezept vor, und dieses Skill wird es auch nicht tun.
Beim Verfassen eines typisierten Schritts run landet man manchmal auf der nicht migrierten Form run(ctx, args), bevor sie vollständig destructured ist. nuka steps --json nennt bereits, welche Fixtures dieser Entwurf berührt, als needs_inferred: eine lexikalische Schätzung, gelesen aus den Member-Zugriffen von ctx selbst, zusammen mit needs: null und einer needs_error, die erklärt, warum der echte Vertrag noch nicht gelesen werden kann. Es erspart das manuelle erneute Lesen des gesamten Körpers, um zu wissen, was zur destructured Signatur hinzugefügt werden muss, aber es ist eine Schätzung, nicht der Vertrag: Es übersieht einen Alias (const c = ctx) und nennt nie needs_browser, also beenden Sie die Destrukturierung, anstatt sie als erledigt zu betrachten.
Wie ein geförderter Schritt aussieht
Ein Beispiel. Vorher, compat-Glue:
const { Given } = require("@cucumber/cucumber");
Given("ein Projekt {string} existiert", async function (name) {
const res = await this.request.post("/projects", { data: { name } });
this.projectId = (await res.json()).id;
});
Nachher, ein typisierter Schritt (features/steps/create-project.ts):
import { defineStep } from "nukadoko";
import { z } from "zod";
export default defineStep({
pattern: "ein Projekt {name:string} existiert",
description: "Ein Projekt erstellen und seine ID zurückgeben",
args: z.object({
name: z.string().describe("Der Anzeigename des Projekts"),
}),
returns: z.object({
id: z.string().describe("Die ID des erstellten Projekts, für spätere Schritte zum Zitieren"),
}),
mutates: true,
async run({ request }, args) {
const res = await request.post("/projects", { data: args });
return res.json();
},
});
Was sich geändert hat:
this.projectIdist weg; die ID kommt stattdessen durchreturnszurück. Ein späterer Schritt deklariertfrom: { projectId: [createProjectStep, "id"] }, um sie per Schlüssel zu lesen –nuka checküberprüft die Bindungsreihenfolge, bevor etwas läuft, und der Lesevorgang erscheint im eigenen Receipt dieses späteren Schritts (dasresultOf-Fixture bleibt für den selteneren Lesevorgang verfügbar, den ein Schlüsselname nicht ausdrücken kann).- Das Argument ist eine benannte Erfassung, die an einen Schemaschlüssel gebunden ist (
{name:string}→args.name), sodass das Muster allein zeigt, welcher Text welches Feld wird. mutates: trueist jetzt eine Deklaration, nicht nur eine Tatsache darüber, was der Code zufällig tut –nuka checkwarnt, wenn dieser Schritt jemals anThengebunden ist, und eine schreibgeschützte Umgebung weigert sich, ihn überhaupt auszuführen. Das Receipt zeichnet trotzdem auf, was der Lauf tatsächlich gesendet hat, zur Überprüfung, aber diese Zählung führt nicht zur Ablehnung des Schritts.this.requestwird zumrequest-Fixture, benannt durch Destrukturierung des ersten Arguments: Nur das, was der Executor tatsächlich injiziert, kann dort benannt werden, nichts Implizites, und nur die tatsächlich benannten Namen werden erstellt. Ein Schritt, der wederpagenochcontextnennt, startet keinen Browser.
Dies ist das einzige ausgearbeitete Beispiel hier. Es ist kein Katalog jeder Lücke zwischen compat und typisiert – wenn ein anderes Muster oft genug auftritt, um eines zu verdienen, ist das eine separate Ergänzung, nichts, was aus diesem einzelnen Fall improvisiert werden sollte.
Wenn ein globaler After-Hook die Bereinigung war
Eine cucumber-js-Suite verlässt sich oft auf einen globalen After-Hook, um aufzuräumen, was ein Schritt erstellt hat: einen Mandanten, eine gesäte Zeile, eine temporäre Datei. Es lohnt sich, dies zu überdenken, sobald der Schritt, der es erstellt hat, gefördert ist: Deklarieren Sie die Ressource stattdessen als Fixture unter nukadoko.config.ts's eigenem fixtures, mit der Bereinigung direkt nach await use(...) in derselben Funktion, die sie gebaut hat, anstatt in einem Hook, der aus Tags allein erraten muss, welche Szenarien sie tatsächlich benötigen. defineFixtures (aus dem nukadoko-Paket) hält das Fixture vollständig typisiert; jeder typisierte Schritt erreicht es auf dieselbe Weise wie page oder request, durch Destrukturierung des Namens. Dies ist additiv, nicht erforderlich: Ein After-Hook, der etwas aufräumt, das noch kein typisierter Schritt beansprucht hat, ist immer noch genau so gültig wie in Phase 1.
Ausgehend von einem DSL für typisierte Schritte
Wenn es keine Feature-Dateien und keinen Cucumber-Glue gibt, ist die compat-Tür in Phase 1 oben nicht relevant – überspringen Sie sie direkt.
Was dies möglich macht, ist, dass pattern bei defineStep optional ist: Ein Schritt kann ohne Muster definiert werden und ist trotzdem ein vollständiges Stück CLI-only-Vokabular, ausführbar mit nuka do und untersuchbar mit nuka describe. Das ist ein anderes Paar von Phasen, die aus demselben Prinzip oben entstehen: Verschieben Sie zuerst jeden Schritt zu einem typisierten defineStep und bündeln Sie ihn später in ein Gherkin-pattern, wann immer eine Feature-Datei es wert macht.
Wenn das Quell-DSL bereits so etwas wie eine description, args, returns, mutates und eine run-Funktion trägt, ist die Übersetzung zu defineStep direkt – jedes hat ein defineStep-Gegenstück, das es empfängt.
Was nicht zu tun ist
- Führen Sie Phase 1 und Phase 2 nicht gleichzeitig durch. Das Mischen eines Importwechsels mit hinzugefügter Typisierung in derselben Änderung ist genau das, was einen Fehler unattributierbar macht – siehe „Zwei Phasen, niemals auf einmal“ oben.
- Lassen Sie die Suite nicht aufhören zu laufen, während sie teilweise migriert ist. Compat- und typisierte Schritte koexistieren in derselben Feature-Datei; eine Suite mit einigen geförderten Schritten und anderen noch im compat-Modus muss während des gesamten Prozesses bestehen bleiben. Machen Sie „vollständig typisiert“ niemals zu einer Vorbedingung für „läuft“.
- Löschen Sie compat-Glue nicht, bevor sein typisierter Ersatz geschrieben ist und läuft. Schreiben Sie den neuen Schritt, bringen Sie ihn mit
nuka dozum Laufen, erst dann entfernen Sie den alten – niemals in der umgekehrten Reihenfolge. - Raten Sie keine Reparatur, die
nuka checkodernuka runnicht angefordert haben. Deren Ausgabe ist der Beweis dafür, was falsch ist; etwas zu ändern, das sie nicht markiert haben, ist eine Änderung auf eine Ahnung hin, nicht auf das, was tatsächlich gebrochen ist.