O problema que BDD resolve
Imagine este cenário: o desenvolvedor implementou exatamente o que estava no ticket. O QA testou e aprovou. O cliente recebeu e disse "não era isso". Ninguém estava errado — cada um entendeu o requisito de uma forma diferente, e ninguém percebeu até ser tarde.
Behavior-Driven Development (BDD) existe para resolver esse problema. Não é uma ferramenta — é uma prática de colaboração que usa uma linguagem compartilhada (Gherkin) para especificar comportamentos que negócio, QA e desenvolvimento conseguem ler, discutir e validar juntos.
O código de teste vira a documentação viva do sistema. E a documentação, por definição, está sempre atualizada — porque se não estiver, o build quebra.
Gherkin: a linguagem do comportamento
Gherkin é uma linguagem estruturada para descrever comportamentos em linguagem natural. A estrutura básica:
Feature: Cálculo de desconto em pedidos
Como cliente da loja
Quero que descontos sejam aplicados automaticamente
Para que eu pague o preço justo pelo meu pedido
Scenario: Pedido acima de R$ 100 recebe 10% de desconto
Given um pedido com valor total de R$ 150,00
And o cliente não é VIP
When o desconto é calculado
Then o desconto deve ser de R$ 15,00
Scenario: Cliente VIP recebe 20% de desconto
Given um pedido com valor total de R$ 200,00
And o cliente é VIP
When o desconto é calculado
Then o desconto deve ser de R$ 40,00
Scenario: Pedidos abaixo de R$ 100 não recebem desconto
Given um pedido com valor total de R$ 80,00
When o desconto é calculado
Then o desconto deve ser zero
Note: qualquer pessoa de negócio consegue ler, entender e validar se os cenários estão corretos — sem saber programar.
Setup do SpecFlow no .NET
dotnet new classlib -n MeuProjeto.Tests.BDD
cd MeuProjeto.Tests.BDD
dotnet add package SpecFlow
dotnet add package SpecFlow.xUnit
dotnet add package SpecFlow.Tools.MsBuild.Generation
dotnet add package FluentAssertions
dotnet add package NSubstitute
<!-- MeuProjeto.Tests.BDD.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SpecFlow.xUnit" Version="3.9.74" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.74" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MeuProjeto.Application\MeuProjeto.Application.csproj" />
<ProjectReference Include="..\MeuProjeto.Domain\MeuProjeto.Domain.csproj" />
</ItemGroup>
</Project>
Criando o arquivo .feature
# Features/Pedidos/CalculoDesconto.feature
Feature: Cálculo de desconto em pedidos
Background:
Given o sistema de descontos está configurado com:
| Valor mínimo | Desconto padrão | Desconto VIP |
| 100.00 | 10% | 20% |
Scenario Outline: Desconto baseado no valor do pedido e perfil do cliente
Given um pedido com valor total de R$ <valor>
And o cliente <perfil_vip>
When o desconto é calculado
Then o desconto deve ser de R$ <desconto_esperado>
Examples:
| valor | perfil_vip | desconto_esperado |
| 150.00 | não é VIP | 15.00 |
| 200.00 | é VIP | 40.00 |
| 80.00 | não é VIP | 0.00 |
| 50.00 | é VIP | 0.00 |
O Scenario Outline com Examples gera um teste para cada linha da tabela — sem duplicar cenários.
Step Definitions: conectando Gherkin ao código
// StepDefinitions/CalculoDescontoSteps.cs
[Binding]
public class CalculoDescontoSteps
{
private readonly ScenarioContext _context;
private CalculadorDesconto _calculador;
private decimal _valorPedido;
private bool _clienteVip;
private decimal _descontoCalculado;
public CalculoDescontoSteps(ScenarioContext context)
{
_context = context;
}
[Given(@"o sistema de descontos está configurado com:")]
public void GivenSistemaConfigurado(Table table)
{
var row = table.Rows[0];
var config = new ConfiguracaoDesconto(
ValorMinimo: decimal.Parse(row["Valor mínimo"]),
PercentualPadrao: ParsePorcentagem(row["Desconto padrão"]),
PercentualVip: ParsePorcentagem(row["Desconto VIP"]));
_calculador = new CalculadorDesconto(config);
}
[Given(@"um pedido com valor total de R\$ (.*)")]
public void GivenPedidoComValor(decimal valor)
{
_valorPedido = valor;
}
[Given(@"o cliente é VIP")]
public void GivenClienteVip()
{
_clienteVip = true;
}
[Given(@"o cliente não é VIP")]
public void GivenClienteNaoVip()
{
_clienteVip = false;
}
[When(@"o desconto é calculado")]
public void WhenDescontoCalculado()
{
_descontoCalculado = _calculador.Calcular(_valorPedido, _clienteVip);
}
[Then(@"o desconto deve ser de R\$ (.*)")]
public void ThenDescontoDeveSerValor(decimal esperado)
{
_descontoCalculado.Should().Be(esperado,
$"pedido de R${_valorPedido} com cliente {(_clienteVip ? "VIP" : "comum")}");
}
[Then(@"o desconto deve ser zero")]
public void ThenDescontoDeveSerZero()
{
_descontoCalculado.Should().Be(0m);
}
private static decimal ParsePorcentagem(string valor) =>
decimal.Parse(valor.Replace("%", "")) / 100m;
}
BDD com dependências externas: DI no SpecFlow
Para cenários mais complexos que precisam de repositórios e serviços, use o container de DI do SpecFlow:
// Support/DependencyInjection.cs
[Binding]
public class DependencyInjectionSupport
{
[BeforeScenario]
public static void RegisterServices(IObjectContainer container)
{
// Fakes para testes de integração isolados
container.RegisterInstanceAs<IPedidoRepository>(new FakePedidoRepository());
container.RegisterInstanceAs<IUnitOfWork>(new FakeUnitOfWork());
container.RegisterInstanceAs<IEmailService>(Substitute.For<IEmailService>());
// Handler real sendo testado
container.RegisterTypeAs<ProcessarPedidoHandler, IRequestHandler<ProcessarPedidoCommand, Result>>();
}
}
// StepDefinitions/ProcessarPedidoSteps.cs
[Binding]
public class ProcessarPedidoSteps
{
private readonly IPedidoRepository _repo;
private readonly IRequestHandler<ProcessarPedidoCommand, Result> _handler;
private readonly IEmailService _emailService;
private Guid _pedidoId;
private Result _resultado;
public ProcessarPedidoSteps(
IPedidoRepository repo,
IRequestHandler<ProcessarPedidoCommand, Result> handler,
IEmailService emailService)
{
_repo = repo;
_handler = handler;
_emailService = emailService;
}
[Given(@"um pedido confirmado aguardando processamento")]
public async Task GivenPedidoConfirmado()
{
_pedidoId = Guid.NewGuid();
var pedido = PedidoBuilder.Novo().ComItem(200m).Confirmado().Build();
await _repo.AddAsync(pedido, default);
}
[When(@"o pedido é processado pelo sistema")]
public async Task WhenPedidoProcessado()
{
_resultado = await _handler.Handle(
new ProcessarPedidoCommand(_pedidoId), default);
}
[Then(@"o cliente deve receber um e-mail de confirmação")]
public async Task ThenEmailEnviado()
{
await _emailService.Received(1)
.NotificarProcessamentoAsync(Arg.Any<Email>(), default);
}
[Then(@"o pedido deve estar com status Processado")]
public async Task ThenStatusAtualizado()
{
var pedido = await _repo.GetByIdAsync(_pedidoId, default);
pedido!.Status.Should().Be(StatusPedido.Processado);
}
}
Feature file para fluxo completo de negócio
Feature: Fluxo de confirmação e processamento de pedido
Scenario: Cliente confirma pedido e recebe e-mail de confirmação
Given um cliente VIP cadastrado com e-mail "joao@empresa.com"
And um pedido em rascunho com 2 itens no valor total de R$ 350,00
When o cliente confirma o pedido
Then o pedido deve ter status "Confirmado"
And o desconto de 20% deve ser aplicado resultando em R$ 70,00 de desconto
And um e-mail de confirmação deve ser enviado para "joao@empresa.com"
And um evento de domínio "PedidoConfirmado" deve ser publicado
Scenario: Tentativa de confirmar pedido sem itens
Given um cliente cadastrado
And um pedido em rascunho sem itens
When o cliente tenta confirmar o pedido
Then a operação deve falhar com mensagem "Um pedido deve ter ao menos um item"
And nenhum e-mail deve ser enviado
Integração com CI/CD e relatórios
O SpecFlow gera relatórios HTML dos cenários executados. Configure no pipeline:
# .github/workflows/bdd.yml
- name: Executar testes BDD
run: dotnet test tests/MeuProjeto.Tests.BDD --logger "trx;LogFileName=bdd-results.trx"
- name: Gerar relatório SpecFlow
run: dotnet specflow nunit-execution-report --testResult tests/bdd-results.trx
- name: Publicar relatório
uses: actions/upload-artifact@v4
with:
name: bdd-report
path: TestResult.html
O relatório final mostra todos os cenários, status (passou/falhou/pendente) e pode ser compartilhado com o time de negócio.
BDD, TDD e Clean Architecture juntos
As três práticas se complementam naturalmente:
BDD na camada de aceitação: define os comportamentos esperados do sistema do ponto de vista do negócio. Testa os casos de uso de fora para dentro.
TDD na camada de domínio e aplicação: guia o design das entidades, value objects e handlers. Testa unidades isoladas com mocks.
Clean Architecture como estrutura: garante que os testes BDD podem ser escritos sem depender de HTTP ou banco — testando direto nos handlers ou com fakes rápidos.
O resultado prático: você tem testes que o negócio pode ler e validar (BDD), testes que guiam o design (TDD) e uma arquitetura que torna ambos fáceis de escrever (Clean Architecture).
Quando BDD faz sentido
BDD agrega mais valor quando: há colaboração real entre negócio e desenvolvimento (o business lê os .feature files), os requisitos são complexos e ambíguos, há um QA envolvido no processo de definição dos cenários, e o sistema tem regras de negócio que mudam com frequência.
BDD agrega menos valor quando: é só o time técnico escrevendo os cenários sem envolvimento do negócio (vira burocracia), os requisitos são triviais (CRUD simples), ou o time ainda não domina TDD (o BDD fica por cima de uma base frágil).
Conclusão
BDD não é uma bala de prata — mas quando aplicado com a colaboração certa, é um dos poucos artefatos de software que alguém de negócio consegue ler, executar e usar para verificar se o sistema está se comportando como deveria.
Os arquivos .feature são um contrato vivo entre negócio e tecnologia. E ao contrário dos documentos de requisitos tradicionais, esse contrato é executável — e quebra o build quando fica desatualizado.