Vos specs Gherkin vous mentent.
Pas intentionnellement. Elles étaient fidèles au départ. Mais six sprints plus tard, quelqu’un a refactoré le flux de checkout et a oublié de mettre à jour le step When the user submits payment. Le fichier .feature passe toujours, parce que la step definition existe toujours. Elle appelle juste du code qui ne correspond plus à ce que le scénario décrit réellement. Vous avez des tests verts et une fausse confiance. C’est la trajectoire par défaut du BDD à moins que vous ne la combattiez activement.
Le problème n’est pas que les développeurs sont paresseux. C’est que la relation entre les fichiers .feature et les step definitions est fondamentalement lâche. Les scénarios Gherkin sont des chaînes de caractères. Les step definitions sont des regexes ou des annotations qui matchent ces chaînes. Aucun compilateur n’impose qu’un changement de scénario nécessite un changement de code correspondant, ou inversement. La chaîne d’outils suppose que vous maintiendrez l’alignement manuellement. Vous ne le ferez pas.
Pourquoi la discipline manuelle échoue à l’échelle
Chaque équipe commence avec le même plan : écrire la spec, implémenter les steps, mettre à jour les deux ensemble. Ça marche la première semaine.
Ça se casse pendant le refactoring. Vous renommez un concept métier dans le code, mais le Gherkin utilise toujours l’ancienne terminologie parce que la changer signifie mettre à jour douze feature files et les refaire relire avec le produit. Ou vous extrayez une nouvelle règle de validation, mais le scénario existant s’appuyait implicitement sur l’ancien comportement, et personne ne l’a remarqué parce que la step definition a été discrètement généralisée pour garder le test passant. Les specs deviennent un univers parallèle, de plus en plus inexact.
Le coût n’est pas seulement une documentation obsolète. C’est la confiance. Une fois que les développeurs cessent de croire que les feature files décrivent la réalité, ils arrêtent de les lire. Puis ils arrêtent de les écrire. Et vous revenez à des unit tests avec des noms opaques et aucun langage partagé avec les stakeholders.
Ce que « en phase » signifie réellement
Maintenir les specs en phase ne signifie pas faire passer les tests. Faire passer, c’est facile. En phase, ça veut dire trois choses :
- Chaque step Gherkin a une step definition correspondante qui fait ce que la spec dit.
- Chaque step definition est réellement atteinte par au moins un scénario.
- Le langage de la spec correspond au langage de la codebase.
La plupart des équipes ne vérifient que le premier point, et elles le font au runtime. Vous devez vérifier les trois, et vous devez le faire en CI avant que le code ne merge.
Validation automatique des steps avec binding strict
Le matching de chaînes lâche dans des outils comme Cucumber est la cause racine. Vous pouvez le resserrer en faisant des step definitions des références de première classe que le build peut valider.
Dans des projets TypeScript ou JavaScript, vous pouvez remplacer les step definitions basées sur des regexes par un registre de steps généré qui mappe les steps Gherkin à de vraies références de fonctions. La clé, c’est que le mapping est généré, pas écrit à la main, donc le build échoue si un scénario référence un step qui n’existe pas.
Voici une configuration minimale utilisant un parser personnalisé et un registre généré. D’abord, parsez vos fichiers .feature au build time :
// 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.`);
Votre registre de steps expose des fonctions par leur texte Gherkin exact :
// 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,
};
Les objets given, when et then sont des modules simples avec des fonctions. Il n’y a pas de magie regex. Si un développeur change le texte Gherkin, il doit ajouter une entrée correspondante au registre, sinon le build échoue. S’il supprime un scénario, la détection de steps orphelins attrape la definition restante.
Intégrez-le à la CI avant le merge
Un script que les développeurs exécutent localement est un script qu’ils oublient d’exécuter. Vous devez faire en sorte que la validation fasse échouer le build.
Ajoutez-le à votre pipeline de tests :
# .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
Le détail important, c’est que validate-steps.ts s’exécute avant la suite de tests réelle. S’il y a un mismatch entre les feature files et les step definitions, vous voulez fail fast avec une erreur claire, pas exécuter une centaine de scénarios Cucumber qui pourraient silencieusement passer sur une logique obsolète.
La living documentation nécessite des rapports générés
La validation garde la syntaxe alignée, mais elle ne garantit pas que les specs soient lisibles ou utiles. Pour ça, vous avez besoin d’une pipeline de living documentation qui génère des rapports HTML à partir de vos feature files et les publie à chaque merge sur main.
Des outils comme Cucumber Reports ou Pickles peuvent transformer vos fichiers .feature en docs navigables. La clé, c’est que les docs sont générées à partir des mêmes fichiers que la CI valide. Si un scénario est supprimé, il disparaît des docs. Si le langage change, les docs se mettent à jour automatiquement. Il n’y a pas de seconde source de vérité à maintenir.
Publiez le rapport comme artifact en CI, ou déployez-le sur un site statique :
# .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
Les stakeholders n’ont pas besoin de lire du Gherkin brut. Ils ont besoin d’une page lisible dont ils savent qu’elle est à jour. L’automatisation construit cette confiance.
Le compromis : rigidité contre expressivité
L’approche par registre a un coût. Vous perdez la flexibilité des patterns regex comme /^the user adds (\d+) items? to the cart$/. Chaque variante devient une entrée explicite, ou un step paramétré avec des placeholders typés. C’est verbeux.
L’alternative est de garder les regexes mais d’ajouter un linter plus strict qui warn quand un pattern est trop large ou quand un texte de step ne matche aucun pattern connu. Vous pouvez obtenir 80 % de la sécurité avec 20 % du verbiage en utilisant les flags dry-run et publish natifs de Cucumber, combinés avec un linter personnalisé qui vérifie les step definitions inutilisées.
# Dry-run parses all features without executing them, surfacing undefined steps
npx cucumber-js --dry-run
C’est moins strict que l’approche par registre. Ça attrape les steps non définis, mais pas les orphelins, et ça n’impose pas l’alignement sémantique. Pour les équipes avec de grandes suites existantes, c’est un point de départ pragmatique. Pour les nouveaux projets, l’approche par registre est rentabilisée en un mois.
Ce que nous avons essayé sans succès
Nous avons expérimenté la génération de Gherkin à partir de commentaires de code. L’idée était que les développeurs annotent leurs méthodes de test, et qu’un outil produise les fichiers .feature. Ça a échoué parce que le Gherkin est censé être lisible par des non-développeurs. La prose générée à partir de noms de méthodes n’est pas lisible. Ce n’est même pas de la prose.
Nous avons aussi essayé d’imposer le pair programming pour chaque changement de spec. Ça a aidé, mais ça n’a pas scale. Le problème est mécanique, et la solution devrait l’être aussi.
Commencez par la détection des steps non définis dès aujourd’hui
Si vous avez une suite Cucumber existante, le plus petit changement utile est d’ajouter --dry-run à votre pipeline CI. Ça prend cinq minutes et ça attrapera la dérive la plus commune : un scénario refactoré qui ne matche plus aucune step definition.
Si vous partez de zéro, envisagez une approche basée sur un registre. Le coût initial des mappings explicites est remboursé par les garanties au build time et la confiance de refactorer librement sans craindre que vos specs deviennent silencieusement obsolètes.
Vos specs Gherkin devraient décrire ce que le système fait. Si vous ne pouvez pas leur faire confiance pour ça, ce ne sont que des commentaires coûteux. Automatisez les vérifications qui les maintiennent honnêtes, ou acceptez qu’elles vous mentiront.