Suas especificações Gherkin estão mentindo para você.
Não intencionalmente. Elas começaram fiéis. Mas seis sprints depois, alguém refatorou o fluxo de checkout e esqueceu de atualizar o passo When the user submits payment. O arquivo .feature ainda passa, porque a definição de passo ainda existe. Ela apenas chama código que não corresponde mais ao que o cenário realmente descreve. Você tem testes verdes e falsa confiança. Essa é a trajetória padrão do BDD, a menos que você a combata ativamente.
O problema não é que os desenvolvedores sejam preguiçosos. É que a relação entre arquivos .feature e definições de passos é fundamentalmente solta. Cenários Gherkin são strings. Definições de passos são regexes ou anotações que correspondem a essas strings. Não há um compilador que force uma mudança de cenário a exigir uma mudança de código correspondente, ou vice-versa. A cadeia de ferramentas assume que você vai mantê-los alinhados manualmente. Você não vai.
Por que a disciplina manual falha em escala
Toda equipe começa com o mesmo plano: escrever a especificação, implementar os passos, atualizar ambos juntos. Isso funciona na primeira semana.
Isso desmorona durante a refatoração. Você renomeia um conceito de domínio no código, mas o Gherkin ainda usa a terminologia antiga porque mudar isso significa atualizar doze arquivos de feature e revisá-los novamente com o produto. Ou você extrai uma nova regra de validação, mas o cenário existente implicitamente dependia do comportamento antigo, e ninguém percebeu porque a definição de passo foi silenciosamente generalizada para manter o teste passando. As especificações se tornam um universo paralelo, cada vez mais impreciso.
O custo não é apenas documentação desatualizada. É confiança. Uma vez que os desenvolvedores param de acreditar que os arquivos de feature descrevem a realidade, eles param de lê-los. Depois param de escrevê-los. Aí você volta a ter testes unitários com nomes opacos e nenhuma linguagem compartilhada com as partes interessadas.
O que “em sincronia” realmente significa
Manter as especificações em sincronia não se trata de fazer os testes passarem. Fazer passar é fácil. Em sincronia significa três coisas:
- Cada passo Gherkin tem uma definição de passo correspondente que faz o que a especificação diz.
- Cada definição de passo é de fato alcançada por pelo menos um cenário.
- A linguagem na especificação corresponde à linguagem na codebase.
A maioria das equipes só verifica o primeiro ponto, e faz isso em tempo de execução. Você precisa verificar os três, e precisa fazer isso na CI antes que o código seja mesclado.
Validação automatizada de passos com binding estrito
A correspondência solta de strings em ferramentas como Cucumber é a causa raiz. Você pode apertá-la tornando as definições de passos referências de primeira classe que o build pode validar.
Em projetos TypeScript ou JavaScript, você pode substituir definições de passos baseadas em regex por um registro de passos gerado que mapeia passos Gherkin para referências de função reais. A chave é que o mapeamento é gerado, não escrito à mão, então o build falha se um cenário referencia um passo que não existe.
Aqui está uma configuração mínima usando um parser customizado e um registro gerado. Primeiro, analise seus arquivos .feature em tempo de build:
// 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.`);
Seu registro de passos expõe funções pelo texto Gherkin exato:
// 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,
};
Os objetos given, when e then são modules simples com funções. Não há mágica de regex. Se um desenvolvedor muda o texto Gherkin, ele deve adicionar uma entrada correspondente ao registro, ou o build falha. Se ele deleta um cenário, a detecção de passos órfãos captura a definição remanescente.
Integre na CI antes do merge
Um script que os desenvolvedores rodam localmente é um script que os desenvolvedores esquecem de rodar. Você precisa fazer a validação falhar o build.
Adicione-o ao seu pipeline de testes:
# .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
O detalhe importante é que validate-steps.ts roda antes da suite de testes real. Se houver uma incompatibilidade entre arquivos de feature e definições de passos, você quer falhar rápido com um erro claro, não rodar cem cenários Cucumber que podem passar silenciosamente em uma lógica obsoleta.
Documentação viva exige relatórios gerados
A validação mantém a sintaxe alinhada, mas não garante que as especificações sejam legíveis ou úteis. Para isso, você precisa de um pipeline de documentação viva que gere relatórios HTML a partir dos seus arquivos de feature e os publique a cada merge na main.
Ferramentas como Cucumber Reports ou Pickles podem transformar seus arquivos .feature em documentos navegáveis. A chave é que os documentos são gerados a partir dos mesmos arquivos que a CI valida. Se um cenário é removido, ele desaparece dos documentos. Se a linguagem muda, os documentos se atualizam automaticamente. Não há uma segunda fonte da verdade para manter.
Publique o relatório como um artefato na CI, ou implante-o em um site estático:
# .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
As partes interessadas não precisam ler Gherkin cru. Eles precisam de uma página legível que eles confiem que está atualizada. Automação constrói essa confiança.
A troca: rigor versus expressividade
A abordagem de registro tem um custo. Você perde a flexibilidade de padrões regex como /^the user adds (\d+) items? to the cart$/. Cada variante se torna uma entrada explícita, ou um passo parametrizado com placeholders tipados. Isso é verboso.
A alternativa é manter as regexes mas adicionar um linter mais rigoroso que avise quando um padrão é muito abrangente ou quando um texto de passo não corresponde a nenhum padrão conhecido. Você pode obter 80% da segurança com 20% da verbosidade usando as flags nativas dry-run e publish do Cucumber, combinadas com um linter customizado que verifica definições de passos não utilizadas.
# Dry-run parses all features without executing them, surfacing undefined steps
npx cucumber-js --dry-run
Isso é menos rigoroso que a abordagem de registro. Ele captura passos indefinidos, mas não órfãos, e não impõe alinhamento semântico. Para equipes com grandes suites existentes, é um ponto de partida pragmático. Para projetos novos, a abordagem de registro se paga dentro de um mês.
O que tentamos e não funcionou
Experimentamos gerar Gherkin a partir de comentários de código. A ideia era que os desenvolvedores anotariam seus métodos de teste, e uma ferramenta produziria os arquivos .feature. Falhou porque Gherkin deveria ser legível por não-desenvolvedores. Prosa gerada a partir de nomes de métodos não é legível. Não é nem prosa.
Também tentamos impor pair programming para cada mudança de especificação. Ajudou, mas não escala. O problema é mecânico, e a correção também deveria ser mecânica.
Comece com a detecção de passos indefinidos hoje
Se você tem uma suite Cucumber existente, a menor mudança útil é adicionar --dry-run ao seu pipeline de CI. Leva cinco minutos e vai capturar a deriva mais comum: um cenário refatorado que não corresponde mais a nenhuma definição de passo.
Se você está começando do zero, considere uma abordagem baseada em registro. O custo inicial de mapeamentos explícitos é pago pelas garantias em tempo de build e pela confiança para refatorar livremente sem se preocupar que suas especificações estejam ficando obsoletas silenciosamente.
Suas especificações Gherkin deveriam descrever o que o sistema faz. Se você não pode confiar que elas façam isso, elas são apenas comentários caros. Automatize as verificações que as mantêm honestas, ou aceite que elas vão mentir para você.