Testes e qualidade de software
Behaviour-driven development com Cucumber e Gherkin
Intermediário7 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
BDD é uma forma de chegar a um acordo sobre o que construir antes que alguém escreva código; o Cucumber é uma maneira de registrar esse acordo para que ele continue verdadeiro. Este guia cobre a conversa de descoberta, a sintaxe Gherkin em detalhe, a ligação dos cenários a step definitions em Java e os deslizes que transformam uma suíte de features em um fardo de manutenção.
O que você vai aprender
- Conduzir uma conversa de descoberta que produza exemplos concretos antes de o código existir
- Escrever Gherkin que descreva o comportamento de negócio, e não os passos da interface do usuário
- Implementar step definitions em Java e compartilhar estado entre elas com segurança
- Usar Scenario Outline, Background, tags e hooks sem duplicar cenários
- Reconhecer os antipadrões que deixam uma suíte Cucumber lenta, frágil e sem leitores
A maioria dos defeitos que importam não são erros de código. São mal-entendidos: o desenvolvedor construiu o que o ticket dizia, o ticket dizia o que o analista entendeu, e o analista entendeu algo um pouco diferente do que o negócio queria dizer. Behaviour-driven development é um conjunto de práticas que mira exatamente nessa lacuna, e sua técnica central é de uma simplicidade desconcertante: antes de escrever código, colocar pessoas com perspectivas diferentes na mesma conversa e obrigá-las a concordar sobre exemplos concretos.
O Cucumber entra depois. Ele pega esses exemplos, escritos em uma linguagem natural estruturada chamada Gherkin, e os executa contra o sistema para que o acordo não possa apodrecer em silêncio. Essa etapa de automação é genuinamente útil, mas também é a mais propensa a ser adotada sem a conversa — e é assim que os times acabam com mil arquivos feature lentos e frágeis que ninguém de fora do time jamais abriu.
Descoberta antes da automação
A conversa tem um formato. Pegue uma user story, reúna em torno dela por meia hora alguém de produto, um desenvolvedor e um testador, e percorra-a com example mapping: a story em um cartão, cada regra de negócio que ela implica em outro, exemplos concretos sob cada regra e, em cartões de uma quarta cor, as perguntas que ninguém ali consegue responder.
O que sai dali raramente são os exemplos. São as perguntas. "O que acontece se o cliente for membro premium e o pedido for internacional?" "Um reembolso parcial reinicia o prazo de devolução?" Cada pergunta levantada naquela sala é um defeito que nunca chegou a ser escrito, e o número de cartões de perguntas é um bom indicador de a story estar pronta ou não: nenhum costuma significar que as regras não foram examinadas; muitos significam que a story deve voltar para ser dividida ou esclarecida.
Os exemplos que sobrevivem viram os critérios de aceitação. Alguns são automatizados como cenários; muitos ficam mais bem cobertos por um teste unitário; alguns descrevem coisas que não vale a pena automatizar. Decidir o que vai para onde é uma questão de julgamento, e errar na direção de "automatizar tudo com o Cucumber" é a forma mais comum de essa prática fracassar.
Gherkin, com precisão
Um arquivo .feature é texto puro com um pequeno conjunto de palavras-chave. Cada arquivo contém uma feature; cada cenário é um exemplo do comportamento.
Feature: Free shipping thresholds
So that customers are encouraged to fill their basket,
orders qualify for free shipping under published rules.
Background:
Given the free shipping threshold is 50 EUR
Scenario: An order above the threshold ships free
Given a basket containing:
| product | quantity | unit price |
| Notebook | 2 | 24.00 |
| Pencil | 1 | 3.50 |
When the customer requests domestic delivery
Then the shipping cost is 0.00 EUR
Scenario: An order below the threshold pays standard shipping
Given a basket containing:
| product | quantity | unit price |
| Pencil | 4 | 3.50 |
When the customer requests domestic delivery
Then the shipping cost is 4.90 EUR
Scenario Outline: Premium members ship free regardless of basket value
Given a <tier> customer with a basket worth <value> EUR
When the customer requests domestic delivery
Then the shipping cost is <cost> EUR
Examples:
| tier | value | cost |
| premium | 10.00 | 0.00 |
| premium | 60.00 | 0.00 |
| standard | 10.00 | 4.90 |
| standard | 60.00 | 0.00 |
As palavras-chave têm funções específicas. Given estabelece o contexto — um estado que já existe, nunca uma ação que o usuário executa. When é o único evento sob teste; um cenário com três passos When na verdade são três cenários, ou um evento mal escolhido. Then verifica um resultado observável, e observável para o ator do cenário: "então a linha da tabela orders tem status PAID" é uma asserção de desenvolvedor fantasiada de Gherkin. And e But dão continuidade à palavra-chave anterior e existem só para facilitar a leitura.
Background reúne os passos Given comuns a todos os cenários do arquivo. É fácil abusar dele: um background de oito passos faz com que cada cenário carregue uma preparação de que a maioria não precisa, e o leitor tem de guardar tudo isso na cabeça. Limite-o a um ou dois passos de contexto realmente universal.
Scenario Outline com uma tabela Examples executa o mesmo cenário uma vez por linha — a ferramenta certa quando uma regra tem vários casos ilustrativos, e a errada para enumerar vinte valores-limite, que pertencem a um teste unitário parametrizado.
Data tables (o bloco delimitado por barras verticais abaixo de um passo) e doc strings (texto entre aspas triplas) passam argumentos estruturados para um passo. Tags (@smoke, @slow, @wip) colocadas acima de uma feature ou de um cenário selecionam o que roda; assim o pipeline pode rodar @smoke a cada commit e tudo durante a noite.
Ligando os cenários ao Java
O Cucumber casa cada linha de passo com métodos anotados. A sintaxe atual usa Cucumber Expressions, muito mais legíveis do que as expressões regulares usadas em projetos mais antigos:
public class ShippingSteps {
private final ShoppingContext context; // shared state, injected
public ShippingSteps(ShoppingContext context) {
this.context = context;
}
@Given("the free shipping threshold is {bigdecimal} EUR")
public void theThresholdIs(BigDecimal threshold) {
context.pricing().setFreeShippingThreshold(Money.of(threshold, "EUR"));
}
@Given("a basket containing:")
public void aBasketContaining(DataTable table) {
for (Map<String, String> row : table.asMaps()) {
context.basket().add(
row.get("product"),
Integer.parseInt(row.get("quantity")),
Money.of(row.get("unit price"), "EUR"));
}
}
@Given("a {word} customer with a basket worth {bigdecimal} EUR")
public void aCustomerWithABasketWorth(String tier, BigDecimal value) {
context.setCustomer(new Customer(Tier.valueOf(tier.toUpperCase(Locale.ROOT))));
context.basket().add("Assorted", 1, Money.of(value, "EUR"));
}
@When("the customer requests {word} delivery")
public void theCustomerRequestsDelivery(String destination) {
context.setQuote(context.shipping().quote(
context.basket(), context.customer(), Destination.valueOf(destination.toUpperCase(Locale.ROOT))));
}
@Then("the shipping cost is {bigdecimal} EUR")
public void theShippingCostIs(BigDecimal expected) {
assertThat(context.quote().shippingCost()).isEqualTo(Money.of(expected, "EUR"));
}
}
Os tipos de parâmetro embutidos cobrem {int}, {word}, {string}, {float} e {bigdecimal}; você registra os seus com @ParameterType para que {money} ou {tier} cheguem como objetos de domínio em vez de strings, o que elimina a maior parte do ruído de parsing dos métodos de passo.
No JUnit 5, o runner usa o engine da plataforma, configurado pela API de suíte ou por junit-platform.properties:
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.shop.acceptance")
@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "not @wip")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber-report.html")
public class RunAcceptanceTests {
}
Compartilhando estado sem campo estático
As step definitions ficam em classes diferentes, e o passo When precisa do que o passo Given criou. A resposta errada — campos estáticos — quebra a execução paralela e vaza estado entre cenários. A resposta do Cucumber é injeção de dependências: adicione um dos módulos cucumber-picocontainer, cucumber-spring ou cucumber-guice, e as classes de step definitions passam a receber injeção via construtor, com um grafo de objetos novo para cada cenário.
public class ShoppingContext { // one instance per scenario
private final Basket basket = new Basket();
private final PricingConfig pricing = new PricingConfig();
private final ShippingService shipping = new ShippingService(pricing);
private Customer customer = Customer.anonymous();
private ShippingQuote quote;
// accessors omitted
}
Com cucumber-spring, anotar uma classe de glue com @CucumberContextConfiguration e @SpringBootTest coloca todo o contexto da aplicação por trás dos passos e transforma cada cenário em uma execução end-to-end de verdade; os trade-offs são os mesmos descritos no guia de testes com Spring Boot. Os hooks (@Before, @After e variantes com tags, como @Before("@database")) cuidam da preparação e da limpeza de cada cenário, e o @After recebe o objeto Scenario, permitindo anexar ao relatório uma captura de tela ou o corpo de uma resposta quando um cenário falha.
Declarativo vence imperativo
O fator que mais determina se uma suíte Cucumber sobrevive é o nível de abstração dos passos. Compare:
# Imperative — describes the interface, not the rule
Scenario: Discount
Given I open "/login"
And I type "[email protected]" into the field "#email"
And I type "hunter2" into the field "#password"
And I click the button "#signin"
And I navigate to "/products/SKU-1"
And I click "Add to basket"
Then the element "#total" contains the text "45.00"
# Declarative — describes the rule, readable by anyone
Scenario: Members get 10% off their first order
Given Ada is a signed-in member with no previous orders
When she adds the 50 EUR notebook to her basket
Then her order total is 45.00 EUR
A versão imperativa quebra quando um designer renomeia uma classe CSS, não diz nada ao leitor sobre a regra de negócio e só roda em um navegador. A versão declarativa sobrevive a redesenhos, declara a regra que protege e pode ser ligada a uma chamada de serviço que roda em milissegundos. Qualquer passo que contenha um seletor, um verbo HTTP, um nome de tabela ou o id de um campo é candidato a ser reescrito um nível acima.
Dito isso, a ligação precisa ser honesta. Um passo chamado "ela adiciona o caderno ao carrinho" tem de passar pelo mesmo caminho de código que a aplicação usa, e não enfiar uma linha direto no banco de dados. Cenários que fingem fazer o trabalho que dizem fazer são piores do que nenhum cenário, porque relatam sucesso em um caminho que nunca foi exercitado.
Documentação viva
É na saída que o BDD mais se diferencia dos testes comuns. O relatório HTML ou JSON do Cucumber, publicado pelo pipeline, mostra cada cenário com seu status; como é gerado a partir dos mesmos arquivos que o time combinou, ele não pode se descolar do código como acontece com uma página de wiki. Os arquivos feature ficam no controle de versão ao lado do código que descrevem, então uma mudança de comportamento e a mudança em sua documentação chegam no mesmo commit e na mesma revisão.
Para isso valer alguma coisa, alguém de fora do time de desenvolvimento precisa de fato lê-la. Se ninguém nunca abre os relatórios, a camada Gherkin é puro overhead, e a mesma cobertura sairia mais barata como testes JUnit comuns — uma conclusão legítima, e melhor reconhecida cedo do que depois de dois anos acumulando arquivos feature.
Antipadrões
Gherkin guiado pela interface. Já tratado acima; é o mais comum e o mais danoso.
Explosão de cenários. Vinte cenários enumerando as combinações de duas flags. Automatize um exemplo ilustrativo por regra e empurre as combinações para uma tabela de decisão no nível unitário, como descrito no guia de fundamentos de testes.
Passos com conjunções. Given the customer is registered and has a valid card and lives in France esconde três fatos em um único passo e não pode ser reutilizado. Divida-o ou introduza um conceito de domínio que os agrupe de forma legítima.
Passos Then técnicos. Verificar linhas do banco, linhas de log ou caminhos JSON puxa o cenário de volta para o nível do desenvolvedor e o torna ilegível para o público a quem ele se destina.
Estado mutável compartilhado entre cenários. Os cenários precisam ser independentes e executáveis em qualquer ordem ou em paralelo. Um cenário que depende de dados criados pelo anterior vai falhar misteriosamente na primeira vez que alguém usar um filtro de tags.
Step definitions com lógica. Laços, condicionais e cálculos dentro de um passo significam que o teste está calculando o resultado esperado em vez de declará-lo. Deixe a aritmética na tabela de exemplos do arquivo feature.
Gherkin escrito só por desenvolvedores. Nesse caso, é apenas um teste JUnit verboso com uma indireção a mais. Envolva os leitores de negócio ou abandone a camada.
Principais conclusões
- O valor do BDD está concentrado na conversa de descoberta; o example mapping traz à tona as perguntas que, de outra forma, virariam defeitos.
- As palavras-chave do Gherkin têm funções precisas:
Givené contexto,Whené um único evento,Thené um resultado que o ator consegue observar. Scenario Outlinecobre um punhado de casos ilustrativos; os limites exaustivos pertencem a testes unitários rápidos, não a arquivos feature.- Compartilhe estado entre step definitions por injeção via construtor, com um objeto de contexto por cenário, nunca por campos estáticos.
- Escreva cenários sobre regras de negócio e ligue-os abaixo da interface do usuário; Gherkin imperativo, clique a clique, é lento, frágil e ilegível.
- Adote o Cucumber só quando pessoas que não desenvolvem leem ou escrevem os cenários — caso contrário, JUnit puro dá a mesma cobertura com menos maquinário.
Fontes e leituras complementares
Perguntas frequentes
- BDD é só TDD com outras palavras-chave?
- Os dois compartilham o ritmo de fora para dentro, mas o centro de gravidade do BDD é a conversa que acontece antes do código. Suas práticas — example mapping, especificação por exemplo, um vocabulário compartilhado — existem para expor mal-entendidos entre produto, desenvolvimento e testes enquanto mudar o requisito ainda é barato. Automatizar os exemplos combinados com o Cucumber é o último passo, e o menos essencial: os times tiram a maior parte do valor da discussão, mesmo que nada seja automatizado.
- Precisamos do Cucumber ou dá para escrever os testes de aceitação em JUnit puro?
- Só adote o Cucumber se pessoas que não são desenvolvedoras realmente lerem ou escreverem os cenários. A camada Gherkin acrescenta uma indireção — glue code, expressões regulares, estado compartilhado — e é pura perda quando os únicos leitores são os desenvolvedores que escreveram o código. Testes JUnit comuns, com métodos bem nomeados e um vocabulário de domínio construído sobre o AssertJ, dão a mesma cobertura com menos maquinário.
- Os cenários devem conduzir a interface do usuário?
- Normalmente não. Cenários escritos como cliques e nomes de campos quebram sempre que o layout muda e rodam devagar em um navegador. Escreva os cenários sobre regras de negócio e ligue-os a um serviço ou a uma API HTTP abaixo da interface; depois, cubra a interface em si com um pequeno número de jornadas end-to-end dedicadas. Assim os arquivos feature continuam legíveis e a suíte fica rápida o bastante para rodar a cada commit.
- Quantos cenários uma feature deve ter?
- Poucos o suficiente para que alguém leia todos. Uma feature que passa de mais ou menos uma dúzia de cenários geralmente está misturando regras que merecem arquivos separados, ou enumerando combinações que pertencem a um teste unitário parametrizado. Use cenários para os exemplos ilustrativos que resolvem uma divergência sobre a regra e cubra os casos-limite exaustivos no nível unitário, onde rodam em milissegundos.