Deine Gherkin-Specs lügen dich an.
Nicht mit Absicht. Sie waren anfangs treu. Aber sechs Sprints später hat jemand den Checkout-Flow refactored und vergessen, den Schritt When the user submits payment zu aktualisieren. Die .feature-Datei läuft immer noch durch, weil die Step Definition weiterhin existiert. Sie ruft nur Code auf, der nicht mehr dem entspricht, was das Szenario tatsächlich beschreibt. Du hast grüne Tests und falsche Zuversicht. Das ist die Standard-Trajektorie von BDD, es sei denn, du bekämpfst sie aktiv.
Das Problem ist nicht, dass Entwickler faul sind. Es liegt daran, dass die Beziehung zwischen .feature-Dateien und Step Definitions grundsätzlich lose ist. Gherkin-Szenarien sind Strings. Step Definitions sind Regexes oder Annotationen, die diese Strings matchen. Es gibt keinen Compiler, der erzwingt, dass eine Szenario-Änderung eine entsprechende Code-Änderung erfordert, oder umgekehrt. Die Toolchain geht davon aus, dass du sie manuell aufeinander abgestimmt hältst. Das wirst du nicht.
Warum manuelle Disziplin im Maßstab versagt
Jedes Team startet mit demselben Plan: Schreibe die Spec, implementiere die Steps, aktualisiere beides zusammen. Das funktioniert in Woche eins.
Es bricht beim Refactoring zusammen. Du benennst ein Domain-Konzept im Code um, aber Gherkin verwendet immer noch die alte Terminologie, denn eine Änderung bedeutet, zwölf Feature-Dateien zu aktualisieren und sie erneut mit dem Product-Team zu reviewen. Oder du extrahierst eine neue Validierungsregel, aber das bestehende Szenario hat implizit auf das alte Verhalten gebaut, und niemand hat es bemerkt, weil die Step Definition stillschweigend generalisiert wurde, um den Test bestehen zu lassen. Die Specs werden zu einem parallelen, zunehmend ungenauen Universum.
Die Kosten sind nicht nur veraltete Dokumentation. Es ist Vertrauen. Sobald Entwickler aufhören zu glauben, dass die Feature-Files die Realität beschreiben, hören sie auf, sie zu lesen. Dann hören sie auf, sie zu schreiben. Dann bist du wieder bei Unit-Tests mit undurchsichtigen Namen und ohne gemeinsame Sprache mit Stakeholdern.
Was “im Takt” tatsächlich bedeutet
Specs im Takt zu halten, bedeutet nicht, die Tests zum Laufen zu bringen. Durchlaufen ist einfach. Im Takt sein bedeutet drei Dinge:
- Jeder Gherkin-Step hat eine entsprechende Step Definition, die das tut, was die Spec beschreibt.
- Jede Step Definition wird tatsächlich von mindestens einem Szenario erreicht.
- Die Sprache in der Spec stimmt mit der Sprache in der Codebase überein.
Die meisten Teams prüfen nur den ersten Punkt, und sie tun es zur Laufzeit. Du musst alle drei prüfen, und du musst es in der CI tun, bevor der Code gemerged wird.
Automatisierte Step-Validierung mit strikter Bindung
Das lose String-Matching in Tools wie Cucumber ist die Ursache. Du kannst es enger ziehen, indem du Step Definitions zu First-Class-Referenzen machst, die der Build validieren kann.
In TypeScript- oder JavaScript-Projekten kannst du regex-basierte Step Definitions durch eine generierte Step Registry ersetzen, die Gherkin-Steps auf tatsächliche Funktionsreferenzen abbildet. Der Schlüssel ist, dass die Zuordnung generiert und nicht per Hand geschrieben wird, sodass der Build fehlschlägt, wenn ein Szenario auf einen Step verweist, der nicht existiert.
Hier ist ein minimales Setup mit einem Custom Parser und einer generierten Registry. Zuerst parst du deine .feature-Dateien zur Build-Zeit:
// scripts/validate-steps.ts
import { readFileSync, readdirSync } from 'fs';
import { parse } from '@cucumber/gherkin';
import { IdGenerator } from '@cucumber/messages';
const featureFiles = readdirSync('./features').filter(f => f.endsWith('.feature'));
const allSteps = new Set<string>();
for (const file of featureFiles) {
const content = readFileSync(`./features/${file}`, 'utf-8');
const gherkinDocument = parse(content, new IdGenerator());
for (const feature of gherkinDocument.feature?.children || []) {
for (const step of feature.scenario?.steps || []) {
allSteps.add(step.text);
}
}
}
// Import the actual step registry from your test code
import { stepRegistry } from '../steps/registry';
const registeredSteps = new Set(Object.keys(stepRegistry));
const undefinedSteps = [...allSteps].filter(s => !registeredSteps.has(s));
const orphanedSteps = [...registeredSteps].filter(s => !allSteps.has(s));
if (undefinedSteps.length > 0) {
console.error('Undefined steps:', undefinedSteps);
process.exit(1);
}
if (orphanedSteps.length > 0) {
console.error('Orphaned steps:', orphanedSteps);
process.exit(1);
}
console.log(`Validated ${allSteps.size} steps against ${registeredSteps.size} definitions.`);
Deine Step Registry stellt Funktionen über ihren exakten Gherkin-Text bereit:
// steps/registry.ts
import { given, when, then } from './step-helpers';
export const stepRegistry: Record<string, Function> = {
'the user is logged in': given.theUserIsLoggedIn,
'the user adds an item to the cart': when.theUserAddsAnItemToTheCart,
'the total should be {int}': then.theTotalShouldBe,
};
Die Objekte given, when und then sind einfache Module mit Funktionen. Es gibt keinen Regex-Zauber. Wenn ein Entwickler den Gherkin-Text ändert, muss er einen entsprechenden Eintrag in die Registry hinzufügen, sonst schlägt der Build fehl. Wenn er ein Szenario löscht, erfasst die Orphaned-Step-Erkennung die übrig gebliebene Definition.
Vor dem Merge in die CI einbinden
Ein Skript, das Entwickler lokal ausführen, ist ein Skript, das Entwickler vergessen auszuführen. Du musst die Validierung zum Build-Fehler machen.
Füge es zu deiner Test-Pipeline hinzu:
# .github/workflows/ci.yml
jobs:
validate-specs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx ts-node scripts/validate-steps.ts
- run: npm test
Das wichtige Detail ist, dass validate-steps.ts vor der eigentlichen Test-Suite läuft. Gibt es eine Diskrepanz zwischen Feature-Files und Step Definitions, willst du schnell mit einem klaren Fehler scheitern, und nicht hundert Cucumber-Szenarien laufen lassen, die möglicherweise stillschweigend auf veralteter Logik durchlaufen.
Living Documentation erfordert generierte Reports
Validierung hält die Syntax im Takt, garantiert aber nicht, dass die Specs lesbar oder nützlich sind. Dafür brauchst du eine Living-Documentation-Pipeline, die HTML-Reports aus deinen Feature-Files generiert und sie bei jedem Merge auf main veröffentlicht.
Tools wie Cucumber Reports oder Pickles können deine .feature-Dateien in durchsuchbare Docs verwandeln. Der Schlüssel ist, dass die Docs aus denselben Dateien generiert werden, die die CI validiert. Wird ein Szenario entfernt, verschwindet es aus den Docs. Ändert sich die Sprache, aktualisieren sich die Docs automatisch. Es gibt keine zweite Quelle der Wahrheit, die gepflegt werden muss.
Veröffentliche den Report als Artifact in der CI, oder deploye ihn auf eine statische Site:
# .github/workflows/docs.yml
jobs:
publish-docs:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: npm install -g @picklesdoc/pickles
- run: pickles --feature-directory=./features --output-directory=./docs
- uses: actions/upload-pages-artifact@v3
with:
path: ./docs
Stakeholder müssen kein rohes Gherkin lesen. Sie brauchen eine lesbare Seite, der sie vertrauen können, dass sie aktuell ist. Automation baut dieses Vertrauen auf.
Der Trade-off: Striktheit versus Ausdruckskraft
Der Registry-Ansatz hat einen Preis. Du verlierst die Flexibilität von Regex-Patterns wie /^the user adds (\d+) items? to the cart$/. Jede Variante wird zu einem expliziten Eintrag oder zu einem parametrisierten Step mit typisierten Platzhaltern. Das ist ausführlich.
Die Alternative ist, Regexes zu behalten, aber einen strengeren Linter hinzuzufügen, der warnt, wenn ein Pattern zu breit ist oder wenn ein Step-Text keinem bekannten Pattern entspricht. Du kannst 80 % der Sicherheit mit 20 % der Ausführlichkeit bekommen, indem du Cucumbers eingebaute Flags dry-run und publish verwendest, kombiniert mit einem Custom Linter, der auf ungenutzte Step Definitions prüft.
# Dry-run parses all features without executing them, surfacing undefined steps
npx cucumber-js --dry-run
Das ist weniger streng als der Registry-Ansatz. Es erfasst undefinierte Steps, aber keine verwaisten, und es erzwingt keine semantische Übereinstimmung. Für Teams mit großen bestehenden Test-Suites ist es ein pragmatischer Startpunkt. Für neue Projekte lohnt sich der Registry-Ansatz innerhalb eines Monats.
Was wir ausprobiert haben, das nicht funktioniert hat
Wir haben experimentiert, Gherkin aus Code-Kommentaren zu generieren. Die Idee war, dass Entwickler ihre Test-Methoden annotieren und ein Tool die .feature-Dateien produziert. Es scheiterte, weil Gherkin von Nicht-Entwicklern lesbar sein soll. Generierte Prosa aus Methodennamen ist nicht lesbar. Es ist nicht einmal Prosa.
Wir haben auch versucht, Pair Programming für jede Spec-Änderung durchzusetzen. Es half, aber es skalierte nicht. Das Problem ist mechanisch, und die Lösung sollte es auch sein.
Starte heute mit der Erkennung undefinierter Steps
Wenn du eine bestehende Cucumber-Suite hast, ist die kleinste nützliche Änderung, --dry-run zu deiner CI-Pipeline hinzuzufügen. Es dauert fünf Minuten und es erfasst die häufigste Drift: ein refactored Szenario, das nicht mehr zu einer Step Definition passt.
Wenn du von vorne anfängst, erwäge einen Registry-basierten Ansatz. Die initialen Kosten expliziter Zuordnungen werden durch Build-Zeit-Garantien und die Zuversicht, frei zu refactoren, ohne dir Sorgen machen zu müssen, dass deine Specs stillschweigend veralten, wieder hereingeholt.
Deine Gherkin-Specs sollten beschreiben, was das System tut. Wenn du ihnen das nicht zutrauen kannst, sind sie nur teure Kommentare. Automatisiere die Prüfungen, die sie ehrlich halten, oder akzeptiere, dass sie dich anlügen werden.