BDD Testes .NET C# SpecFlow

BDD com SpecFlow e Gherkin no .NET: testes que o negócio entende e o código executa

Como implementar BDD no .NET com SpecFlow e Gherkin: escrever cenários Given-When-Then, integrar com xUnit, e usar BDD para alinhar negócio e desenvolvimento desde o início.

N
Neryx Digital Architects
15 de março de 2026
12 min de leitura
190 profissionais leram
Categoria: .NET Público: Times de engenharia e produto Etapa: Aprendizado

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.

Quer transformar esse aprendizado em plano de ação?

Se o tema deste artigo se parece com o momento do seu time, podemos ajudar a decidir o próximo passo com clareza.

Falar com um especialista

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.