← Todos os artigos

Seu time não faz BDD. Faz Gherkin.

A conversa entre negócio, dev e QA é o produto. O arquivo .feature é a sobra dela. Quem inverte isso escreve Cypress em português e chama de especificação.

Isso aqui saiu de uma suíte de verdade. Troquei o nome do módulo e mais nada:

features/recebimento.feature
Funcionalidade: Recebimento

  Cenário: Salvar recebimento
    Dado que eu estou na tela "Recebimento de Insumos"
    E que eu clico no botão "Novo"
    E que eu aguardo 2 segundos
    E que eu preencho o campo id-produto com "12345"
    E que eu preencho o campo qtd-recebida com "180"
    E que eu seleciono "Sim" no combo cmb-conferido
    Quando eu clico no botão "Salvar"
    Então eu vejo a div de sucesso
    E o campo id-produto está vazio

Eram 31 cenários no mesmo arquivo, todos com essa cara. Rodavam em 14 minutos. Quebravam sempre que alguém mexia num id do HTML.

Fica um tempo olhando. Sem pressa.

O que exatamente está podre ali

Não é feiura. É estrutura errada, e dá pra apontar com o dedo.

Cada passo descreve interface, não comportamento. "Clico no botão Salvar" não é uma regra de negócio — é o gesto que hoje aciona a regra. Amanhã a tela vira um wizard de três etapas e o cenário quebra sem que nada do negócio tenha mudado. Você acoplou a especificação ao DOM, e DOM é a camada mais volátil que existe no produto.

id-produto e cmb-conferido estão dentro de um arquivo que teoricamente serve pra conversar com quem entende de agronomia. Nenhum comprador de insumo no Brasil sabe o que é cmb-conferido.

E o Então é o pior de todos: "eu vejo a div de sucesso". Sucesso de quê? Se o sistema gravar o recebimento com a quantidade errada e mostrar o toast verde, esse teste passa. Ele valida que o feedback visual apareceu. Não valida absolutamente nada sobre o que aconteceu com o estoque, com a ordem de compra ou com a divergência.

Tecnicamente: passo irreutilizável (nasce grudado numa tela), acoplamento ao seletor, asserção em pixel, e — o buraco de verdade — zero regra de negócio expressa. Se você apagar o arquivo, não perde conhecimento nenhum. Todo o conhecimento que existia ali já estava no código de produção.

Agora o mesmo caso, escrito no nível em que ele deveria ter nascido:

features/conferencia-romaneio.feature
Funcionalidade: Conferência de romaneio no recebimento

  Regra: Falta acima da tolerância do fornecedor vira divergência

    Cenário: Romaneio conferido com quantidade menor que a pedida
      Dado uma ordem de compra aprovada de 200 L do defensivo "Glifosato 480 SL"
      E que o fornecedor não tem tolerância de quebra contratada
      Quando o romaneio é conferido com 180 L
      Então a diferença de 20 L é registrada como divergência de recebimento
      E a ordem de compra permanece com 20 L em aberto

    Cenário: Falta dentro da tolerância contratada
      Dado uma ordem de compra aprovada de 200 L do defensivo "Glifosato 480 SL"
      E que o fornecedor tem tolerância de quebra de 2%
      Quando o romaneio é conferido com 197 L
      Então nenhuma divergência é registrada
      E a ordem de compra é encerrada

Repara no que sumiu: tela, botão, campo, espera, seletor. E no que apareceu: tolerância de quebra, saldo em aberto, encerramento da ordem. O segundo cenário não existia no arquivo original — ele apareceu porque escrever a regra obriga alguém a perguntar "e quando a falta é pequena?".

Esse par de arquivos é o artigo inteiro. O resto é explicação.

BDD nasceu de um problema de ensino, não de ferramenta

Dan North começou a falar de BDD lá por 2003, e o motivo era prosaico: ele estava tentando ensinar TDD e as pessoas emperravam sempre nos mesmos lugares. Por onde começar. O que testar e o que não testar. Como chamar o teste. Por que o teste falhou, quando falhou.

A percepção dele foi que a palavra "teste" atrapalhava. Ela puxa a cabeça de quem está aprendendo pra verificação — algo que se faz depois, sobre código que já existe. Trocando "teste" por "comportamento", a conversa mudava de lugar: o nome do método vira uma frase sobre o que o sistema deveria fazer, e aí a pergunta "por onde começo" se responde sozinha. Daí veio o JBehave, daí veio o vocabulário de should, e daí, junto com o trabalho do Chris Matts sobre valor e critério de aceite, veio o formato dado/quando/então.

O centro de gravidade sempre foi linguagem. Linguagem ubíqua, no sentido do Eric Evans: um vocabulário único que negócio, código e teste compartilham, sem tradutor no meio. O Gherkin, que só apareceu depois com o Cucumber, é uma tentativa de dar sintaxe a isso. É uma consequência. Virou a coisa toda porque sintaxe é o que dá pra copiar de um blog e colocar no repositório na segunda-feira. Conversa não dá pra instalar via npm.

A conversa dura 40 minutos, não um dia

Three Amigos é o nome que pegou pra reunião entre quem entende do negócio, quem vai escrever o código e quem vai testar. Três papéis, não necessariamente três pessoas.

Todo mundo que tentou e desistiu desistiu pela mesma razão: transformou em workshop. Meio dia de sala, post-it, facilitador. Isso morre em duas sprints porque ninguém tem meio dia.

O formato que sobrevive é curto. Uma história, 30 a 45 minutos, três pessoas, antes de qualquer linha de código. E o que faz render não é técnica de facilitação, são umas poucas perguntas chatas:

  • Me dá um exemplo com número. Não "o sistema calcula a divergência", mas "200 pedidos, 180 chegaram, o que acontece". Número force decisão; abstração esconde desacordo.
  • E se não for assim? Chegou mais do que o pedido. Chegou em dois romaneios no mesmo dia. Chegou depois da ordem já encerrada. Chegou em quilo quando a ordem estava em litro.
  • Quem descobre esse problema e quando? O conferente na balança, na hora, ou o financeiro trinta dias depois, na conciliação da nota?
  • Isso já aconteceu? Essa é a minha favorita e ela vem direto dos anos que eu passei em suporte. Se já aconteceu, existe um chamado, e o chamado tem o dado feio de verdade — que quase nunca é o dado que o PO imaginou. É o mesmo trabalho de traduzir relato de usuário em reprodução, só que feito antes do bug existir.

Na última dessas que eu participei, sobre justamente conferência de romaneio, a pergunta "e se chegar mais do que o pedido?" travou a sala por uns bons minutos. O PO não sabia. Ninguém sabia. Foi perguntar pro time de compras e voltou com uma regra que existia no contrato com os fornecedores desde 2019 e não estava em lugar nenhum do sistema.

Aquela pergunta valeu mais que a sprint inteira. E ela não gerou .feature nenhum.

Na prática

O retorno do BDD é a descoberta do caso de borda antes de ele virar chamado. Isso acontece na conversa, com três pessoas numa sala, e continua acontecendo mesmo que nenhum arquivo Gherkin seja escrito depois. O arquivo é opcional. A conversa não é.

A conta que ninguém faz antes de adotar

Cada passo em português precisa de uma definição em código. Essa é a parte que aparece no tutorial. O que não aparece é o que acontece com essas definições no mês oito.

Alguém escreve "que eu preencho o campo X com Y". Três sprints depois outra pessoa precisa de "que eu preencho o campo X com Y sem limpar antes". Ninguém quer duplicar step, então entra um grupo opcional na regex. Depois precisa esperar um pouco. Entra outro grupo opcional. Depois precisa guardar o valor pra conferir num passo seguinte, e como o step não retorna nada, o valor vai pro estado compartilhado.

cypress/support/step_definitions/recebimento.ts
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor';

// o Cypress não tem World, então a gente inventou um.
// isso vive no escopo do módulo e sobrevive entre cenários. sim, vaza.
const mundo: Record<string, unknown> = {};

// nasceu como 'que eu preencho o campo X com "Y"'.
// hoje aceita: sem limpar, com espera, e guardando o valor.
Given(
  /^que eu preencho o campo ([\w-]+) com "([^"]*)"(?: sem limpar antes)?(?: e aguardo (\d+) segundos)?(?: guardando como (\w+))?$/,
  (campo: string, valor: string, espera?: string, apelido?: string) => {
    const seletor = mapaDeCampos[campo] ?? `[data-testid="${campo}"]`;
    const limpar = !Cypress.currentTest.title.includes('sem limpar antes');

    const alvo = cy.get(seletor);
    if (limpar) alvo.clear();
    alvo.type(valor);

    if (espera) cy.wait(Number(espera) * 1000);   // nunca mais alguém tirou
    if (apelido) mundo[apelido] = valor;
  },
);

// e este aqui é o passo que só funciona se o de cima rodou antes,
// coisa que o arquivo .feature não tem como te avisar
Then(/^o valor guardado como (\w+) aparece no resumo$/, (apelido: string) => {
  cy.contains('[data-testid="resumo"]', String(mundo[apelido])).should('be.visible');
});

Olha o Cypress.currentTest.title.includes('sem limpar antes'). O step definition está lendo o texto do próprio cenário pra decidir o que fazer, porque a regex casa o grupo opcional mas ninguém se lembrou de capturá-lo. Eu não inventei isso pra ilustrar. Eu já escrevi coisa pior.

Esse é o custo real: a camada de tradução vira um projeto paralelo, com bug próprio, sem teste próprio, mantida por gente que já tem outro trabalho. E o cy.wait fixo ali no meio é como instabilidade entra e se instala — alguém pôs pra "resolver" uma corrida, funcionou naquele dia, e virou parte da linguagem.

Agora o mesmo caso, sem camada nenhuma:

cypress/e2e/recebimento-divergencia.cy.ts
describe('Conferência de romaneio', () => {
  beforeEach(() => cy.session('comprador', () => cy.loginComoComprador()));

  it('registra divergência quando o romaneio vem com falta acima da tolerância', () => {
    cy.criarOrdemDeCompra({
      produto: 'Glifosato 480 SL',
      quantidade: 200,
      unidade: 'L',
      toleranciaQuebraPct: 0,
      status: 'APROVADA',
    }).then((ordem) => {
      cy.conferirRomaneio(ordem.id, { quantidadeRecebida: 180 });

      cy.visit(`/recebimento/${ordem.id}`);
      cy.contains('[data-testid="divergencia"]', '20 L').should('be.visible');

      cy.buscarOrdem(ordem.id).its('saldoEmAberto').should('eq', 20);
    });
  });
});

Vinte linhas contra o arquivo .feature mais o arquivo de steps mais o mapa de campos mais o objeto de estado. O nome do it diz a regra inteira. Quando quebra, o stack trace aponta pra linha certa em vez de apontar pra uma regex.

E eu preciso ser honesto sobre o que se perdeu: o comprador não lê isso. O arquivo Gherkin, mesmo ruim, tem uma chance de ser lido por alguém de fora. Esse aqui não tem nenhuma. Se o seu contexto é um em que essa leitura acontece de verdade, você acabou de jogar fora uma coisa que valia. Só que "de verdade" está fazendo um trabalho pesado nessa frase.

Onde a camada paga por si

Situação Gherkin vale? Por quê
Regra de negócio densa, com muita combinação (tolerância, rateio, tributação por estado) Vale O cenário é o único formato em que o especialista consegue conferir a combinação sem ler código
Domínio com vocabulário próprio — safra, talhão, ano-safra, romaneio, defensivo Vale O arquivo vira o lugar onde o termo tem definição executável. "Ano-safra" deixa de ser folclore
Existe stakeholder que abre o arquivo e comenta nele Vale muito É o único caso em que você recebe o benefício que justifica a sintaxe
Regressão de regra que já causou prejuízo Vale O cenário legível é o registro do acordo, não só a proteção
Fluxo de UI, navegação, validação de formulário Não Não tem regra pra expressar. Só tem gesto, e gesto muda toda release
CRUD Não "Dado que eu cadastro, então está cadastrado" não é conhecimento de ninguém
Teste técnico — contrato de API, migração, idempotência Não O público é engenheiro. Escrever em português só adiciona uma tradução
Ninguém de negócio tem acesso ao repositório Não Você está escrevendo carta pra um endereço que não existe

Isso é a mesma lógica de olhar risco em vez de olhar formato: a pergunta não é qual proporção de cenários Gherkin o time deveria ter, é qual regra específica precisa ser legível por quem não programa.

O critério dos três meses

Vou te dar um teste de uma linha. Abra o histórico do repositório e responda: alguém de fora da engenharia leu, comentou ou alterou um .feature nos últimos três meses?

Não vale "o PO participou do refinamento". Não vale "eu mostrei numa reunião". Leu o arquivo. Falou alguma coisa sobre o conteúdo dele.

Se a resposta é não, a camada Gherkin é custo puro. Apague. Escreva o teste direto, no nome do it, com a regra explicada em português na descrição — o que, aliás, é uma prática que ninguém deveria ter abandonado.

Vai doer, porque tem trabalho investido ali. Trabalho investido não é argumento. É a definição de custo afundado.

"Mas o Gherkin é documentação viva"

Esse é o contra-argumento sério e eu não vou fingir que é fraco. A ideia — que vem do pessoal do Cucumber, e o Matt Wynne desenvolve bem isso — é que a especificação em linguagem natural, amarrada a código executável, nunca fica desatualizada. Se a regra muda e ninguém atualiza o cenário, a suíte fica vermelha. Documentação que se recusa a mentir.

O mecanismo é real. Só que ele garante uma coisa e uma só: que o texto corresponde ao sistema. Não garante que o texto seja compreensível, nem relevante, nem lido.

Documentação viva precisa de leitor pra estar viva. Sem leitor, o que você tem é um documento que se atualiza sozinho e não informa ninguém — documentação em coma. Sinais vitais estáveis, atividade cerebral zero. E ela custa CI, custa revisão de PR, custa o tempo de quem tenta entender por que o step 47 quebrou.

Onde o crítico tem razão de verdade — e aqui eu recuo: em domínio regulado, ou onde existe auditoria, o cenário legível tem valor mesmo sem leitura frequente. Se daqui a dois anos aparecer um auditor perguntando como o sistema tratava quebra de defensivo no recebimento em 2026, um arquivo em português que rodava verde naquela data é uma resposta muito melhor que um expect em TypeScript. O leitor existe, ele só é raro e chega tarde. Nesse caso a documentação não está em coma, está de plantão.

A distinção que importa: você está mantendo o Gherkin porque ele é lido, ou porque alguém pode precisar ler? A primeira justifica sozinha. A segunda justifica um subconjunto pequeno — as regras que dão processo, multa ou prejuízo. Não os 31 cenários de tela de cadastro.

Eu levei tempo demais pra admitir

A suíte que eu descrevi lá em cima teve uma segunda vida, e a segunda vida foi minha.

Eu peguei aquilo, olhei os 31 cenários horrorosos e fiz a coisa que parecia certa: reescrevi tudo no nível da regra. Passei perto de três semanas construindo uma camada de steps limpa, com nomes de domínio, sem seletor vazando, tudo bonito. Fiquei genuinamente orgulhoso. Mostrei em uma tech talk interna.

Sete meses depois eu fui olhar o histórico do diretório features/ por outro motivo. Cada commit era de um engenheiro. Cada um. O PO nunca tinha aberto. O analista de negócio nem sabia que a pasta existia.

Eu tinha construído um tradutor caprichado entre dois idiomas falados pelas mesmas pessoas.

E o pior não é ter feito. O pior é que eu suspeitei disso lá pelo mês três e passei mais quatro meses arrumando desculpa — "falta a gente divulgar melhor", "quando o time de produto crescer". Defendi em retrospectiva. Era teatro, e eu era o único na plateia batendo palma.

A conversa dos 40 minutos com o pessoal de compras, aquela que descobriu a regra de tolerância de 2019: essa eu continuo fazendo até hoje, toda história que tem regra de verdade. Ela nunca gerou um arquivo .feature.

Ela é o BDD. O resto era formatação.

Luciano Martins

Luciano Martins da Silva Junior

Bacharel em Engenharia de Software, pós-graduado em Ciência de Dados e Machine Learning. 8+ anos entre suporte, automação de testes e engenharia de dados. Escreve sobre o que quebra em produção.

Tem um problema difícil de qualidade?

Suíte instável, pipeline lento, dado que mente. É esse tipo de coisa que eu gosto de desembaraçar.

Vamos conversar