Como estruturar testes por comportamento com BDD e Cucumber

1. Fundamentos do BDD e o papel do Cucumber

Behavior-Driven Development (BDD) é uma abordagem de desenvolvimento ágil que incentiva a colaboração entre desenvolvedores, QA e stakeholders de negócio. Seus princípios fundamentais incluem:

  • Linguagem ubíqua: todos os envolvidos usam o mesmo vocabulário para descrever comportamentos
  • Foco no comportamento: os cenários descrevem o que o sistema deve fazer, não como
  • Colaboração contínua: os cenários são escritos antes do código, como especificações executáveis

A principal diferença entre BDD e TDD está no escopo: enquanto TDD foca em unidades de código (funções, métodos), BDD descreve comportamentos de alto nível que atravessam múltiplas camadas. Use BDD quando precisar alinhar equipes multidisciplinares e validar requisitos de negócio.

O Cucumber é o framework mais popular para BDD. Sua estrutura básica inclui:

  • Arquivos .feature: cenários escritos em Gherkin
  • Step definitions: código que implementa os passos Gherkin
  • Hooks: funções de setup/teardown (Before, After)

2. Escrevendo cenários com Gherkin de forma eficaz

A sintaxe básica do Gherkin utiliza palavras-chave como Feature, Scenario, Given, When, Then, Background e Scenario Outline.

Exemplo de cenário bem escrito:

Feature: Login de usuário
  Como um usuário registrado
  Quero fazer login no sistema
  Para acessar minhas funcionalidades

  Background:
    Dado que existem usuários cadastrados:
      | email              | senha   |
      | joao@email.com     | abc123  |
      | maria@email.com    | xyz789  |

  Scenario: Login bem-sucedido
    Dado que estou na página de login
    Quando preencho o campo "email" com "joao@email.com"
    E preencho o campo "senha" com "abc123"
    E clico no botão "Entrar"
    Então devo ver a mensagem "Bem-vindo, João"

Boas práticas para cenários legíveis:

  • Evite detalhes de implementação (não mencione IDs de elementos, classes CSS)
  • Use exemplos concretos com dados reais
  • Mantenha cenários atômicos (um comportamento por cenário)

Uso de Scenario Outline para múltiplas variações:

Scenario Outline: Login com diferentes credenciais
  Dado que estou na página de login
  Quando preencho o campo "email" com "<email>"
  E preencho o campo "senha" com "<senha>"
  E clico no botão "Entrar"
  Então devo ver a mensagem "<mensagem>"

  Examples:
    | email              | senha   | mensagem          |
    | joao@email.com     | abc123  | Bem-vindo, João   |
    | maria@email.com    | xyz789  | Bem-vinda, Maria  |
    | invalido@email.com | 123     | Credenciais inválidas |

3. Organização de features e cenários em projetos reais

A estrutura de diretórios recomendada organiza features por domínio ou funcionalidade:

projeto/
├── features/
│   ├── login/
│   │   ├── login.feature
│   │   └── step_definitions/
│   │       └── login_steps.py
│   ├── pagamento/
│   │   ├── pagamento.feature
│   │   └── step_definitions/
│   │       └── pagamento_steps.py
│   └── support/
│       ├── hooks.py
│       └── environment.py
├── src/
└── config/

Uso de tags para organizar cenários:

@smoke @login
Scenario: Login com credenciais válidas
  ...

@regression @pagamento
Scenario: Processar pagamento com cartão de crédito
  ...

Para executar subconjuntos:

# Executar apenas cenários @smoke
cucumber --tags @smoke

# Executar cenários @regression, exceto @wip
cucumber --tags @regression and not @wip

Gerenciamento de dependências: evite acoplamento entre cenários usando hooks para setup isolado:

# hooks.py
Before:
  - Limpar banco de dados de teste
  - Criar dados básicos necessários

After:
  - Capturar screenshot se falhou
  - Limpar dados do cenário

4. Implementação de step definitions com Cucumber

Mapeamento de passos Gherkin para código usando Cucumber Expressions:

# login_steps.py
from behave import given, when, then

@given('que estou na página de login')
def step_impl(context):
    context.browser.get('http://localhost:3000/login')

@when('preencho o campo "{campo}" com "{valor}"')
def step_impl(context, campo, valor):
    element = context.browser.find_element_by_name(campo)
    element.send_keys(valor)

@then('devo ver a mensagem "{mensagem}"')
def step_impl(context, mensagem):
    assert mensagem in context.browser.page_source

Compartilhamento de estado entre steps usando o objeto context:

# environment.py
def before_scenario(context, scenario):
    context.usuario_logado = None
    context.dados_teste = {}

@given('que estou logado como "{tipo_usuario}"')
def step_impl(context, tipo_usuario):
    context.usuario_logado = criar_usuario(tipo_usuario)
    realizar_login(context, context.usuario_logado)

Uso de hooks avançados:

# hooks.py
from behave import fixture, use_fixture

@fixture
def setup_browser(context):
    context.browser = webdriver.Chrome()
    yield context.browser
    context.browser.quit()

def before_all(context):
    use_fixture(setup_browser, context)

def after_step(context, step):
    if step.status == "failed":
        context.browser.save_screenshot(f"screenshots/{step.name}.png")

5. Integração com frameworks de automação e asserções

Conectando Cucumber com Selenium para testes web:

# web_steps.py
@given('que acesso o site "{url}"')
def step_impl(context, url):
    context.browser.get(url)

@when('clico no botão "{texto}"')
def step_impl(context, texto):
    button = context.browser.find_element_by_xpath(f"//button[text()='{texto}']")
    button.click()

Para testes de API com RestAssured (Java):

# api_steps.java
@Quando("envio uma requisição POST para {string} com o corpo:")
public void enviarPost(String endpoint, String body) {
    response = RestAssured.given()
        .contentType(ContentType.JSON)
        .body(body)
        .post(endpoint);
}

@Então("o status code deve ser {int}")
public void validarStatusCode(int statusCode) {
    assertEquals(statusCode, response.getStatusCode());
}

Asserções comportamentais com AssertJ:

@Então("a resposta deve conter o campo {string} com valor {string}")
public void validarCampo(String campo, String valor) {
    assertThat(response.jsonPath().getString(campo))
        .isEqualTo(valor);
}

6. Estratégias de manutenção e reuso de cenários

Criação de steps genéricos reutilizáveis:

@given('que estou logado como "{tipo}"')
def step_impl(context, tipo):
    credenciais = {
        "admin": {"email": "admin@email.com", "senha": "admin123"},
        "usuario": {"email": "user@email.com", "senha": "user123"},
        "convidado": {"email": "guest@email.com", "senha": "guest123"}
    }
    dados = credenciais[tipo]
    realizar_login(context, dados["email"], dados["senha"])

Versionamento de features: mantenha um arquivo de mapeamento entre histórias de usuário e cenários:

# user_stories_mapping.txt
US-001: Login de usuário -> features/login/login.feature
US-002: Cadastro de usuário -> features/cadastro/cadastro.feature
US-003: Processar pagamento -> features/pagamento/pagamento.feature

Evite fragilidade focando em comportamento, não em implementação:

# Ruim: dependente de implementação
@when('clico no elemento com CSS "#btn-login-123"')

# Bom: focado no comportamento
@when('clico no botão "Entrar"')

7. Execução e relatórios para equipes não-técnicas

Configuração de runner com JUnit e Cucumber Reports:

# TestRunner.java
@RunWith(Cucumber.class)
@CucumberOptions(
    features = "src/test/resources/features",
    glue = "stepdefinitions",
    tags = "@regression",
    plugin = {
        "pretty",
        "html:target/cucumber-reports",
        "json:target/cucumber.json",
        "junit:target/cucumber.xml"
    }
)
public class TestRunner {}

Geração de relatórios legíveis com Allure:

# Adicionar dependência no pom.xml
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-cucumber5-jvm</artifactId>
    <version>2.24.0</version>
</dependency>

Integração contínua com CI/CD:

# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Cucumber tests
        run: mvn test -Dcucumber.filter.tags="@smoke"
      - name: Generate report
        run: mvn allure:report

8. Armadilhas comuns e como evitá-las

Cenários muito longos: prefira cenários atômicos com no máximo 5-7 passos.

# Ruim: cenário monolítico
Scenario: Fluxo completo do usuário
  Given que acesso o site
  When faço login
  And adiciono produto ao carrinho
  And finalizo compra
  And acesso meus pedidos
  Then vejo o pedido confirmado

# Bom: cenários atômicos
Scenario: Login bem-sucedido
  Given que acesso o site
  When faço login com credenciais válidas
  Then vejo a página inicial

Scenario: Adicionar produto ao carrinho
  Given que estou logado
  When adiciono o produto "Camiseta" ao carrinho
  Then o carrinho deve conter 1 item

Dependência de ordem de execução: use dados isolados e evite estado global.

# Ruim: dependente de cenário anterior
@when('utilizo o ID do pedido criado anteriormente')

# Bom: dados isolados por cenário
@given('que crio um novo pedido')
def step_impl(context):
    context.pedido_id = criar_pedido_unico()

Excesso de abstração: equilibre reuso e legibilidade.

# Ruim: genérico demais
@when('executo ação "{acao}" no elemento "{elemento}"')

# Bom: específico e legível
@when('clico no botão "Enviar"')
@when('preencho o campo "Email" com "{email}"')

Referências