Cucumber 7.34 + Spring Boot 3: BDD do zero em 15 minutos - @CucumberContextConfiguration e JUnit 5 - Meu Universo Nerd

Existe um jeito de escrever testes que o PO entende, o QA valida e o dev executa, tudo com o mesmo arquivo. Com Cucumber 7.34 e Spring Boot 3, o setup completo leva menos de 15 minutos. A maioria dos devs Java ainda não usa, e esse artigo vai mudar isso.

Existe um jeito de escrever testes que o PO entende, o QA valida e o dev executa, tudo com o mesmo arquivo. Com Cucumber 7.34 e Spring Boot 3, o setup completo leva menos de 15 minutos. A maioria dos devs Java ainda não usa, e esse artigo vai mudar isso.

Nesse artigo você vai aprender: o que é BDD e quando faz sentido usar, como configurar Cucumber 7.34 com Spring Boot 3 e JUnit 5 corretamente, como escrever feature files em Gherkin legíveis, como criar step definitions com injeção de dependência Spring, e como integrar tudo com @CucumberContextConfiguration para evitar o NullPointerException que quebra 90% dos projetos na primeira tentativa. Bora?

O que é BDD e por que Cucumber ainda domina em 2026

BDD (Behavior-Driven Development) é uma abordagem de desenvolvimento onde os testes de aceitação são escritos em linguagem natural estruturada antes do código funcional existir. Parece teoria, mas na prática é uma das ferramentas mais poderosas para alinhar time técnico e negócio sem reuniões intermináveis.

Pensa assim: quando o PO escreve uma história de usuário ("Como usuário, quero fazer login para acessar minha conta"), ela existe no Jira, passa pelo refinamento, vira sprint, vira código, e depois... alguém escreve um teste unitário que o PO nunca vai ver. O BDD quebra esse ciclo. Com Cucumber, o teste de aceitação É a especificação, escrita em Gherkin:


Feature: Login de usuário

  Scenario: Login com credenciais válidas
    Given que o usuário "Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo." está cadastrado com senha "senha123"
    When o usuário tenta fazer login com email "Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo." e senha "senha123"
    Then o sistema retorna HTTP 200 com token JWT válido

  Scenario: Login com senha incorreta
    Given que o usuário "Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo." está cadastrado com senha "senha123"
    When o usuário tenta fazer login com email "Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo." e senha "errada"
    Then o sistema retorna HTTP 401 com mensagem "Credenciais inválidas"

Esse arquivo, o PO lê, o QA assina, e o dev implementa. Sem tradução, sem lacuna de interpretação.

Sobre por que Cucumber em 2026: o ecossistema amadureceu muito. A versão 7.34.3 (março 2026) tem integração nativa com JUnit Platform 1.14, suporte a PicoContainer melhorado, e o @BeforeStep/@AfterStep agora entrega o Step info completo para logging e diagnóstico. Não é mais aquele Cucumber complicado de 2018.

Configurando o projeto: pom.xml do jeito certo

Muita gente erra aqui: mistura versões de artefatos Cucumber ou usa a integração JUnit 4 em projetos Spring Boot 3 (que usa JUnit 5 por padrão). Resultado: testes que não rodam, ClassNotFoundExceptions e horas de debugging desnecessário.

A regra de ouro: todas as dependências Cucumber devem estar na mesma versão. Hoje, 7.34.3. Segue o bloco correto para Spring Boot 3 com JUnit 5:


<properties>
    <cucumber.version>7.34.3</cucumber.version>
    <junit.platform.version>1.11.4</junit.platform.version>
</properties>

<dependencies>
    <!-- Spring Boot Test (já inclui JUnit 5) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Cucumber Core -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>${cucumber.version}</version>
        <scope>test</scope>
    </dependency>

    <!-- Cucumber + JUnit 5 (JUnit Platform Engine) -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <version>${cucumber.version}</version>
        <scope>test</scope>
    </dependency>

    <!-- Cucumber + Spring (compartilhamento de contexto) -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-spring</artifactId>
        <version>${cucumber.version}</version>
        <scope>test</scope>
    </dependency>

    <!-- JUnit Platform Suite (para Suite Runner) -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Repara: não tem cucumber-junit aqui (esse é o JUnit 4). Para Spring Boot 3, o correto é cucumber-junit-platform-engine. Esse detalhe específico é responsável por metade dos tickets de "Cucumber não encontra meus steps" que vejo no Stack Overflow.

Estrutura de pastas e arquivos

O Cucumber espera uma estrutura específica dentro de src/test/. Na empresa em que trabalhei, o time costumava jogar tudo em src/test/java e depois ficava horas entendendo por que o Cucumber não achava os feature files. A estrutura correta:


src/
  test/
    java/
      com/example/
        CucumberTestSuite.java     <-- Suite runner
        config/
          CucumberSpringConfig.java <-- Configuração do Spring
        steps/
          LoginSteps.java           <-- Step definitions
    resources/
      features/
        login.feature               <-- Feature files aqui!
      application-test.yml          <-- Config de teste

Feature files SEMPRE em src/test/resources/features/. O classpath do Cucumber busca a partir do classpath raiz, e o Maven coloca src/test/resources no classpath de teste automaticamente. Certinho?

A anotação que resolve tudo: @CucumberContextConfiguration

Aqui mora o segredo que a maioria dos tutoriais pula. Sem @CucumberContextConfiguration, o Spring Boot não sabe que precisa subir o contexto para os testes Cucumber. O resultado: @Autowired retornando null, beans não encontrados, e NullPointerException sem mensagem útil.

A classe de configuração tem uma responsabilidade única: ser a "ponte" entre o Cucumber e o Spring Boot:


package com.example.config;

import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

@CucumberContextConfiguration
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfig {
    // Não precisa de nada aqui.
    // A magia é nas anotações.
}

E o Suite Runner que conecta tudo com o JUnit 5:


package com.example;

import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;

import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.steps,com.example.config")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber-reports/report.html")
public class CucumberTestSuite {
    // Classe vazia. O JUnit 5 usa as anotações.
}

O GLUE_PROPERTY_NAME aponta tanto para o pacote de steps quanto para o de config. Sem o pacote de config no glue, o @CucumberContextConfiguration não é encontrado e voltamos ao NullPointerException.

Escrevendo Step Definitions com injeção de dependência

Agora o código que implementa cada passo do feature file. Com cucumber-spring no classpath, você pode usar @Autowired normalmente nas classes de step definition:


package com.example.steps;

import com.example.service.AuthService;
import com.example.dto.LoginRequest;
import com.example.dto.LoginResponse;
import io.cucumber.java.pt.Dado;
import io.cucumber.java.pt.Quando;
import io.cucumber.java.pt.Entao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

public class LoginSteps {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    private ResponseEntity<LoginResponse> response;

    @Dado("que o usuário {string} está cadastrado com senha {string}")
    public void usuarioCadastrado(String email, String senha) {
        userRepository.createUser(email, senha);
    }

    @Quando("o usuário tenta fazer login com email {string} e senha {string}")
    public void usuarioFazLogin(String email, String senha) {
        var request = new LoginRequest(email, senha);
        response = restTemplate.postForEntity("/api/auth/login", request, LoginResponse.class);
    }

    @Entao("o sistema retorna HTTP {int} com token JWT válido")
    public void verificaLoginSucesso(int statusCode) {
        assertThat(response.getStatusCode())
            .isEqualTo(HttpStatus.valueOf(statusCode));
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().token()).isNotBlank();
    }

    @Entao("o sistema retorna HTTP {int} com mensagem {string}")
    public void verificaLoginErro(int statusCode, String mensagem) {
        assertThat(response.getStatusCode())
            .isEqualTo(HttpStatus.valueOf(statusCode));
    }
}

Repara no detalhe: io.cucumber.java.pt.Dado, Quando, Entao. O Cucumber 7+ suporta anotações em português nativamente! Isso significa que você escreve o feature file em PT-BR e os step definitions também ficam em português, sem precisar configurar nada extra. Bonito, tá?

Rodando os testes e lendo o relatório HTML

Com tudo configurado, basta rodar:


mvn test -Dtest=CucumberTestSuite

Ou para rodar com tag específica (só cenários marcados como @smoke, por exemplo):


mvn test -Dtest=CucumberTestSuite -Dcucumber.filter.tags="@smoke"

O relatório HTML é gerado em target/cucumber-reports/report.html. Ele mostra cada cenário, cada passo, tempo de execução e o motivo de falha quando algo quebra. Muito melhor que o output do JUnit puro para conversar com o PO sobre o que está ou não implementado.

Como Tech Leader, aprendi que apresentar esse relatório para o time de produto toda sprint muda a conversa. Ao invés de "os testes estão passando" você mostra "os cenários de negócio 1, 2 e 3 estão validados, o 4 ainda não está implementado". Linguagem de negócio, não de engenharia.

Cleanup e boas práticas para não criar acoplamento

Um erro comum em projetos com banco de dados real nos testes: não limpar o estado entre cenários. O Cucumber executa os cenários em sequência, então dados criados no Scenario 1 podem "vazar" para o Scenario 2 e causar falhas intermitentes (os famosos testes que "passam sozinho mas falham em sequência").

Use @Before e @After do Cucumber (não do JUnit) para gerenciar o estado:


import io.cucumber.java.Before;
import io.cucumber.java.After;
import io.cucumber.java.Scenario;

public class DatabaseHooks {

    @Autowired
    private UserRepository userRepository;

    @Before
    public void limparBancoPorScenario() {
        userRepository.deleteAll();
    }

    @After
    public void logScenarioResult(Scenario scenario) {
        if (scenario.isFailed()) {
            // Cucumber 7.34: scenario.getStepResults() disponível
            scenario.log("Scenario falhou: " + scenario.getName());
        }
    }
}

E para testes que precisam de dados fixos (seeds), prefira @BeforeAll + banco em memória (H2) a depender de dados no banco de desenvolvimento. Isso mantém os testes independentes do ambiente.


Perguntas Frequentes sobre Cucumber + Spring Boot

1. Posso usar Cucumber com banco de dados real em vez de H2?
Sim, mas com cuidado. Use um banco dedicado para testes (ex: container Docker via Testcontainers) com setup/teardown controlado. Em produção, H2 é mais rápido e isolado para testes unitários de BDD. Para testes de integração reais, Testcontainers com PostgreSQL é a abordagem que sêniores Java usam hoje.

2. Feature files devem ser escritos em inglês ou português?
Depende do time. Se o PO lê inglês, inglês. Se não lê, português. O Cucumber 7+ suporta as duas línguas nativamente com anotações @Dado, @Quando, @Entao para PT-BR. O mais importante é que o arquivo seja entendido por quem não é dev.

3. NullPointerException no @Autowired dentro dos steps: o que fazer?
Quase sempre é ausência do @CucumberContextConfiguration na classe de config, ou o pacote dessa classe não está no GLUE_PROPERTY_NAME da suite. Verifique os dois antes de qualquer outra coisa.

4. Cucumber substitui testes unitários com JUnit?
Não. Cucumber opera na camada de testes de aceitação (comportamento de negócio). JUnit opera na camada unitária (lógica isolada). Os dois coexistem, cada um com seu papel na pirâmide de testes.

5. Como rodar apenas cenários específicos no CI/CD?
Use tags no feature file (@smoke, @regressao) e filtre com -Dcucumber.filter.tags="@smoke". No pipeline, você pode ter uma etapa de smoke tests rápida e uma de regressão completa.


BDD com Cucumber não é sobre escrever mais testes. É sobre escrever os testes certos, no nível certo de abstração, e que sirvam como documentação viva do sistema. Com Cucumber 7.34 e Spring Boot 3, o setup ficou mais limpo do que nunca.

Se você quer se aprofundar no próximo nível, o passo natural é integrar Cucumber com Testcontainers para testes de integração reais e com Serenity BDD para relatórios ainda mais ricos para stakeholders. Faz sentido cobrir isso aqui? Comenta embaixo que eu preparo o próximo artigo da série.

Segue o Meu Universo Nerd para mais conteúdo Java real, sem HelloWorld.