Testes Automatizados com xUnit: Mudar Sem Medo

Testes Automatizados com xUnit: Mudar Sem Medo

Como montar um projeto de testes com xUnit para a Livraria: Fact e Theory, testes de exceções e eventos, TheoryData para valores decimal, dublês de teste, o relógio falso do TimeProvider, testes assíncronos e por que 100% de cobertura não garante que o código foi verificado.
Linguagem C#

• • 19 min de leitura

Um sistema sem testes automatizados é um sistema em que cada mudança é uma aposta; com eles, centenas de verificações rodam em segundos e avisam no instante exato em que algo deixou de funcionar.

"Interop com C" terminou com uma constatação: a Livraria já tem dezenas de classes, regras de negócio, código assíncrono, travas e até uma fronteira com código nativo, e até agora verificamos tudo lendo a saída no console. Em "Interfaces" e em "Enums e Pattern Matching" chegamos a improvisar verificações com if, prometendo que um dia elas virariam algo sistemático. Esse dia é hoje. Nesta aula, que fecha a Fase 4, montamos um projeto de testes com o xUnit, o framework de testes mais usado no ecossistema .NET, escrevemos testes para as regras mais importantes da Livraria e conhecemos as ferramentas que complementam os testes na garantia de qualidade.

Por que testes automatizados

Um teste automatizado é um pedaço de código que executa outro pedaço de código e verifica se o resultado é o esperado. Os benefícios vão além de "encontrar bugs":

  • Proteção contra regressões. A razão principal. Quando você altera o cálculo de desconto, os testes confirmam em segundos que o frete, o estoque e o ciclo de vida do pedido continuam funcionando. Sem eles, cada mudança exige testar tudo à mão, e na prática ninguém testa tudo.
  • Retorno sobre o desenho. Código difícil de testar costuma ser código mal desenhado: dependências escondidas, responsabilidades misturadas. A classe Pedido, que recebe a política de desconto por interface, é fácil de testar por causa das escolhas de "Interfaces".
  • Documentação executável. Um teste chamado Vender_AlemDoEstoque_LancaExcecao descreve uma regra de negócio, e, diferente de um comentário, ele não fica desatualizado sem que alguém perceba.

Os testes se dividem conforme o alcance. Testes de unidade verificam uma classe ou método isolado, sem banco, rede ou disco, e rodam em milissegundos. Testes de integração verificam várias peças juntas, como o código e um banco de dados de verdade, e são mais lentos. A proporção saudável é muitos testes de unidade, alguns de integração e poucos testes de ponta a ponta, na forma da chamada "pirâmide de testes". Nesta aula ficamos nos testes de unidade. Os de integração voltam na fase de bancos de dados e na de API.

O projeto de testes

Os testes moram num projeto separado, que referencia o projeto testado. Na raiz da solução de "Do Arquivo Único à Solução":

dotnet new xunit -n Livraria.Dominio.Testes -o tests/Livraria.Dominio.Testes
dotnet sln add tests/Livraria.Dominio.Testes
dotnet add tests/Livraria.Dominio.Testes reference src/Livraria.Dominio

O SDK do .NET traz o modelo xunit pronto. A versão 3 do xUnit, a mais recente, tem modelos próprios, instaláveis com dotnet new install xunit.v3.templates. A sintaxe desta aula vale para as duas versões. A estrutura da solução fica com o código em src/ e os testes em tests/, uma convenção comum no ecossistema .NET. Para rodar todos os testes da solução:

dotnet test

O primeiro teste

Um teste no xUnit é um método público marcado com [Fact]:

using Livraria.Dominio.Produtos;

namespace Livraria.Dominio.Testes;

public class LivroImpressoTestes
{
    [Fact]
    public void Vender_ComEstoqueSuficiente_ReduzOEstoque()
    {
        // Preparar
        var livro = new LivroImpresso("Dom Casmurro", "Machado de Assis", 59.90m, pesoGramas: 350, estoque: 10);

        // Agir
        livro.Vender(3);

        // Verificar
        Assert.Equal(7, livro.Estoque);
    }
}

Duas convenções aparecem aqui. A estrutura em três blocos, Preparar, Agir, Verificar (em inglês, Arrange, Act, Assert), deixa cada teste com uma forma previsível: monta o cenário, executa uma ação e confere o resultado. E o nome segue o padrão Metodo_Cenario_ResultadoEsperado, para que, quando um teste falhar, o nome já diga o que quebrou, sem precisar abrir o código.

Assert.Equal(esperado, obtido) compara os dois valores e, se forem diferentes, faz o teste falhar com uma mensagem mostrando ambos. A ordem importa para a clareza da mensagem: primeiro o esperado, depois o obtido. O xUnit oferece verificações para cada necessidade: Assert.True, Assert.Null e Assert.NotNull, Assert.Contains e Assert.Empty para coleções, Assert.InRange.

Testando exceções e eventos

Uma regra de negócio que proíbe algo precisa de um teste que confirme a proibição. Assert.Throws<T> executa uma ação, exige que ela lance exatamente o tipo indicado e devolve a exceção para verificações adicionais:

[Fact]
public void Vender_AlemDoEstoque_LancaExcecaoComOsDados()
{
    var livro = new LivroImpresso("Dom Casmurro", "Machado de Assis", 59.90m, pesoGramas: 350, estoque: 2);

    var excecao = Assert.Throws<EstoqueInsuficienteException>(() => livro.Vender(5));

    Assert.Equal(2, excecao.Disponivel);
    Assert.Equal(5, excecao.Solicitado);
    Assert.Equal(2, livro.Estoque);   // a venda recusada não alterou nada
}

A última linha merece atenção: ela verifica que uma operação que falhou não deixou o objeto num estado parcial. É o tipo de garantia que só um teste escrito de propósito confirma.

Eventos se testam inscrevendo uma lambda que registra o que recebeu:

[Fact]
public void Vender_AoAtingirOLimite_DisparaEstoqueBaixo()
{
    var livro = new LivroImpresso("Dom Casmurro", "Machado de Assis", 59.90m, pesoGramas: 350, estoque: 3);
    EstoqueBaixoEventArgs? recebido = null;
    livro.EstoqueBaixo += (_, e) => recebido = e;

    livro.Vender(1);   // restam 2, que é o limite padrão

    Assert.NotNull(recebido);
    Assert.Equal(2, recebido.Restante);
}

Testes parametrizados: [Theory]

Muitas regras precisam ser verificadas com vários valores. Em vez de copiar o mesmo teste, marque-o com [Theory] e forneça os casos:

[Theory]
[InlineData("978-0-306-40615-7")]
[InlineData("9780306406157")]
public void Construtor_ComIsbnValido_Aceita(string texto)
{
    var isbn = new Isbn(texto);
    Assert.Equal("9780306406157", isbn.Valor);
}

[Theory]
[InlineData("978-0-306-40615-8")]   // dígito verificador errado
[InlineData("978030640615")]        // 12 dígitos
[InlineData("97803064061X7")]       // letra no meio
public void Construtor_ComIsbnInvalido_LancaArgumentException(string texto)
{
    Assert.Throws<ArgumentException>(() => new Isbn(texto));
}

Cada [InlineData] vira uma execução separada do teste, com seu próprio resultado no relatório. Há uma limitação que pega todo mundo na primeira vez: os argumentos de atributos, como vimos em "Reflexão e Atributos", precisam ser constantes gravadas nos metadados, e decimal não pode ser constante de atributo. [InlineData(59.90m)] não compila. Para valores decimal, ou para objetos, use [MemberData] com um TheoryData fortemente tipado:

public class DescontoPorVolumeTestes
{
    public static TheoryData<int, decimal> Faixas => new()
    {
        { 1, 0m }, { 2, 0m },      // abaixo de 3: sem desconto
        { 3, 10m }, { 9, 10m },    // de 3 a 9: 10%
        { 10, 15m }, { 50, 15m },  // 10 ou mais: 15%
    };

    [Theory]
    [MemberData(nameof(Faixas))]
    public void Calcular_SobreCemReais_AplicaOPercentualDaFaixa(int quantidade, decimal descontoEsperado)
    {
        var politica = new DescontoPorVolume();

        ResultadoDesconto resultado = politica.Calcular(100m, quantidade);

        Assert.Equal(descontoEsperado, resultado.Valor);
    }
}

Repare na escolha dos casos: os valores nas fronteiras das faixas (2 e 3, 9 e 10) são os mais importantes. Um erro de > no lugar de >= quase sempre aparece exatamente na fronteira. E o nameof protege a referência ao membro de dados contra renomeações, como vimos na aula de reflexão.

Dependências falsas

Em "Interfaces" escrevemos a política DescontoFixoDeTeste para verificar o Pedido sem depender das regras reais de desconto. Isso tem nome: é um dublê de teste, uma implementação substituta de uma dependência, controlada pelo teste. Com ela, o teste do Pedido verifica só a responsabilidade do pedido:

public class PedidoTestes
{
    private sealed class DescontoFixo(decimal valor) : IPoliticaDesconto
    {
        public ResultadoDesconto Calcular(decimal subtotal, int quantidadeTotal) => new("fixo", valor);
    }

    [Fact]
    public void Pagar_PedidoVazio_LancaInvalidOperation()
    {
        var pedido = new Pedido();
        Assert.Throws<InvalidOperationException>(() => pedido.Pagar());
        Assert.Equal(StatusPedido.Aberto, pedido.Status);
    }

    [Fact]
    public void Cancelar_PedidoJaEnviado_NaoEPermitido()
    {
        var pedido = new Pedido();
        pedido.Adicionar(new Ebook("Vidas Secas", "Graciliano Ramos", 24.90m, 1.8), 1);
        pedido.Pagar();
        pedido.Enviar();

        Assert.Throws<InvalidOperationException>(() => pedido.Cancelar());
        Assert.Equal(StatusPedido.Enviado, pedido.Status);
    }
}

Para interfaces pequenas, dublês escritos à mão, como a classe aninhada DescontoFixo, são simples e claros. Para interfaces grandes, ou quando o teste precisa verificar como uma dependência foi chamada (quantas vezes, com quais argumentos), existem bibliotecas que criam dublês automaticamente, como a NSubstitute. Use-as com moderação: testes que verificam cada chamada interna ficam presos à implementação e quebram a cada refatoração, mesmo quando o comportamento não mudou.

O tempo como dependência

Um tipo de dependência engana muita gente: o relógio. Um código que chama DateTime.Now diretamente não pode ser testado de forma confiável. O teste de "cupom expirado" só passaria depois da data de validade, e o de "cupom válido", só antes dela. O .NET 8 introduziu a abstração TimeProvider exatamente para isso. O código recebe o relógio como dependência:

public sealed class CupomComValidade(string codigo, decimal percentual,
                                     DateTimeOffset validoAte, TimeProvider relogio) : IPoliticaDesconto
{
    public ResultadoDesconto Calcular(decimal subtotal, int quantidadeTotal) =>
        relogio.GetUtcNow() <= validoAte
            ? new ResultadoDesconto($"cupom {codigo}", subtotal * percentual)
            : new ResultadoDesconto($"cupom {codigo} expirado", 0m);
}

Em produção, passa-se TimeProvider.System, o relógio real. No teste, um relógio falso, que o próprio teste controla, do pacote Microsoft.Extensions.TimeProvider.Testing:

using Microsoft.Extensions.Time.Testing;

[Fact]
public void Calcular_DepoisDaValidade_NaoDaDesconto()
{
    var relogio = new FakeTimeProvider(new DateTimeOffset(2026, 9, 1, 0, 0, 0, TimeSpan.Zero));
    var cupom = new CupomComValidade("PRIMAVERA", 0.20m,
                                     validoAte: new DateTimeOffset(2026, 9, 30, 23, 59, 59, TimeSpan.Zero),
                                     relogio);

    relogio.Advance(TimeSpan.FromDays(40));   // "viaja" para outubro

    Assert.Equal(0m, cupom.Calcular(100m, 1).Valor);
}

É a mesma ideia do exercício de "Classes e Objetos" em que Idade recebia o ano atual por parâmetro: tornar explícito o que era uma dependência escondida.

Testes assíncronos

Métodos de teste podem ser assíncronos: basta declará-los como async Task. Para exceções assíncronas, existe Assert.ThrowsAsync:

[Fact]
public async Task CarregarAsync_ArquivoInexistente_DevolveListaVazia()
{
    string caminho = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.json");

    List<Produto> produtos = await ArquivoDeCatalogo.CarregarAsync(caminho);

    Assert.Empty(produtos);
}

Guid.NewGuid() gera um identificador único, garantindo um caminho que certamente não existe e que não colide com outro teste rodando ao mesmo tempo. O xUnit executa classes de teste diferentes em paralelo por padrão, e testes que tocam o mesmo arquivo ou o mesmo recurso compartilhado podem interferir uns nos outros, em versões intermitentes dos problemas de "Concorrência". Cada teste deve preparar o seu próprio cenário, sem depender de outros nem da ordem de execução.

Cobertura: uma medida útil e perigosa

O modelo de projeto já inclui um coletor de cobertura de código, que mede quais linhas foram executadas pelos testes:

dotnet test --collect:"XPlat Code Coverage"

A cobertura é útil para encontrar código que nenhum teste executa, o que aponta regiões sem proteção. Mas ela é perigosa como meta. Uma linha executada não é uma linha verificada: um teste que chama Vender e não tem nenhum Assert eleva a cobertura sem verificar nada. Equipes que perseguem "100% de cobertura" costumam produzir exatamente esse tipo de teste. A pergunta certa não é "quantas linhas foram executadas?", mas "se eu introduzir um bug aqui, algum teste falha?". Ferramentas de testes de mutação, como o Stryker.NET, respondem a essa pergunta alterando o código de propósito e verificando se os testes percebem.

Qualidade além dos testes

Testes verificam comportamento. Outras ferramentas verificam o próprio código, e custam quase nada para ligar.

Analisadores. O SDK do .NET inclui centenas de regras de análise de código, construídas sobre a mesma infraestrutura do Roslyn que os geradores de "Source Generators" usam. Elas detectam usos incorretos de APIs, problemas de desempenho, de segurança e de globalização. O nível é ajustado no Directory.Build.props:

<PropertyGroup>
  <AnalysisLevel>latest-recommended</AnalysisLevel>
  <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

Estilo consistente. Um arquivo .editorconfig na raiz da solução define as convenções de formatação e de nomes da equipe, e o comando dotnet format aplica essas regras a todo o código. Discussões sobre onde vai a chave ou quantos espaços de indentação desaparecem: a ferramenta decide.

Combinados com o TreatWarningsAsErrors que ligamos na Fase 2, os analisadores e os testes formam uma rede de proteção que roda a cada compilação. Na fase de API, faremos essa rede rodar automaticamente a cada envio de código para o repositório.

Juntando tudo: a Livraria testada

Com os testes das seções anteriores, a execução mostra algo como:

$ dotnet test
Restauração concluída.
  Livraria.Dominio -> .../Livraria.Dominio.dll
  Livraria.Dominio.Testes -> .../Livraria.Dominio.Testes.dll
Aprovado! – Com falha: 0, Aprovado: 18, Ignorado: 0, Total: 18, Duração: 48 ms

Dezoito casos de teste (os [Fact] e cada linha de cada [Theory]) em menos de um décimo de segundo. Agora faça uma experiência: em DescontoPorVolume, troque >= 10 por > 10. Rode dotnet test. O caso { 10, 15m } falha imediatamente, com uma mensagem dizendo que o esperado era 15 e o obtido foi 10. Um erro que, sem testes, só apareceria quando um cliente comprasse exatamente dez livros e reclamasse do desconto, se reclamasse.

Com os testes, a Fase 4 está completa, e ela levou você ao nível do runtime. Você sabe onde cada dado mora na memória e quanto custa cada alocação, e escreve código que processa milhões de registros sem criar lixo com Span<T>. Os dados do programa sobrevivem em arquivos, em formatos que outros sistemas leem. As esperas por entrada e saída não bloqueiam threads, várias operações rodam ao mesmo tempo, com cancelamento e limites, e os dados compartilhados entre threads não se corrompem. Você atravessou a fronteira com o código nativo e cercou tudo isso com testes automatizados. São as competências que separam quem escreve programas que funcionam de quem escreve programas que continuam funcionando sob carga, ao longo do tempo e depois de mudanças.

Mas o catálogo da Livraria ainda é um arquivo JSON, lido inteiro na inicialização e regravado inteiro a cada mudança. Dois caixas vendendo ao mesmo tempo, em computadores diferentes, sobrescreveriam as mudanças um do outro. Perguntas como "o faturamento de cada autor no último trimestre" exigem carregar tudo na memória e calcular com LINQ. E nada impede que um arquivo editado à mão contenha uma venda de um produto que não existe. Dados que muitas pessoas consultam e alteram ao mesmo tempo, com regras de integridade e perguntas complexas, pedem uma ferramenta feita exatamente para isso.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva os testes do objeto de valor Cep de "Source Generators": uma teoria para formatos válidos, que devem normalizar para oito dígitos, e outra para formatos inválidos, incluindo um CEP com dígitos árabes-índicos.

Ver resposta

✓ Resposta:

public class CepTestes
{
    [Theory]
    [InlineData("80010-000")]
    [InlineData("80010000")]
    [InlineData("  80010-000  ")]
    public void Construtor_FormatoValido_NormalizaParaOitoDigitos(string texto)
    {
        var cep = new Cep(texto);
        Assert.Equal("80010000", cep.Valor);
        Assert.Equal("80010-000", cep.ToString());
    }

    [Theory]
    [InlineData("8001-0000")]
    [InlineData("80010-00")]
    [InlineData("8001O-000")]              // letra O no lugar do zero
    [InlineData("\u0668\u0660\u0660\u0661\u0660-\u0660\u0660\u0660")]   // dígitos árabes-índicos
    [InlineData("")]
    public void Construtor_FormatoInvalido_LancaArgumentException(string texto)
    {
        Assert.ThrowsAny<ArgumentException>(() => new Cep(texto));
    }
}

O caso com espaços confirma que o Trim funciona. O caso com dígitos árabes-índicos (\u0660 a \u0669) protege a decisão de usar [0-9] em vez de \d na expressão regular: se alguém "simplificar" o padrão no futuro, esse teste falha e explica por quê. Todos esses casos, inclusive a string vazia, lançam exatamente ArgumentException, e Throws bastaria. O ThrowsAny, que aceita também as derivadas, protege o caso que ficou de fora: null, para o qual o ThrowIfNullOrWhiteSpace lança ArgumentNullException, e Throws<ArgumentException>, que exige o tipo exato, falharia. Esses testes documentam as regras do CEP melhor que qualquer comentário.

Exercício 2

Por que [InlineData(59.90m)] não compila? Mostre duas formas de testar um método que recebe decimal com vários valores.

Ver resposta

✓ Resposta: Os argumentos de um atributo precisam ser constantes que o compilador consiga gravar nos metadados do assembly, e a especificação do C# só permite tipos primitivos como int, double, bool, string, char, enums e Type. decimal é uma struct de 16 bytes sem representação constante nos metadados, então não é aceito. A primeira forma, recomendada, é TheoryData com [MemberData]:

public static TheoryData<decimal, decimal> Precos => new() { { 59.90m, 53.91m }, { 100m, 90m } };

[Theory, MemberData(nameof(Precos))]
public void AplicarDezPorCento(decimal preco, decimal esperado) =>
    Assert.Equal(esperado, preco * 0.90m);

A segunda é passar os valores como string no InlineData e convertê-los no teste com decimal.Parse(texto, CultureInfo.InvariantCulture). Funciona, mas perde a verificação de tipo e depende de a conversão estar certa. A cultura invariante é obrigatória aqui, pela armadilha de "Operadores, Texto e Conversa com o Usuário": o mesmo teste passaria numa máquina em inglês e falharia numa em português. TheoryData é tipado, legível e aceita qualquer tipo, inclusive objetos do domínio.

Exercício 3

Escreva testes para o ciclo de vida do Pedido cobrindo: não é possível adicionar itens a um pedido pago; um pedido aberto pode ser cancelado; um pedido entregue não pode ser cancelado.

Ver resposta

✓ Resposta:

public class CicloDeVidaDoPedidoTestes
{
    private static Pedido PedidoComUmItem()
    {
        var pedido = new Pedido();
        pedido.Adicionar(new Ebook("Vidas Secas", "Graciliano Ramos", 24.90m, 1.8), 1);
        return pedido;
    }

    [Fact]
    public void Adicionar_EmPedidoPago_LancaInvalidOperation()
    {
        var pedido = PedidoComUmItem();
        pedido.Pagar();

        Assert.Throws<InvalidOperationException>(
            () => pedido.Adicionar(new Ebook("São Bernardo", "Graciliano Ramos", 22.90m, 1.6), 1));
        Assert.Single(pedido.Itens);
    }

    [Fact]
    public void Cancelar_PedidoAberto_MudaParaCancelado()
    {
        var pedido = PedidoComUmItem();
        pedido.Cancelar();
        Assert.Equal(StatusPedido.Cancelado, pedido.Status);
    }

    [Fact]
    public void Cancelar_PedidoEntregue_LancaInvalidOperation()
    {
        var pedido = PedidoComUmItem();
        pedido.Pagar();
        pedido.Enviar();
        pedido.ConfirmarEntrega();

        Assert.Throws<InvalidOperationException>(() => pedido.Cancelar());
        Assert.Equal(StatusPedido.Entregue, pedido.Status);
    }
}

O método auxiliar PedidoComUmItem remove a repetição da preparação, e cada teste cria o seu próprio pedido, sem compartilhar estado. Em todos os casos de falha, o teste verifica também que o estado não mudou: a quantidade de itens continua 1 e o status continua o anterior. Os três testes correspondem a linhas da tabela TransicaoPermitida de "Enums e Pattern Matching". Se alguém alterar a tabela por engano, o teste correspondente aponta exatamente qual regra quebrou.

Exercício 4

O teste abaixo passa quase sempre, mas falha em alguns dias do ano. Explique e corrija o código testado e o teste.

public sealed class Promocao(DateTime inicio, DateTime fim)
{
    public bool Ativa => DateTime.Now >= inicio && DateTime.Now <= fim;
}

[Fact]
public void Ativa_DentroDoPeriodo()
{
    var promocao = new Promocao(DateTime.Today, DateTime.Today.AddDays(30));
    Assert.True(promocao.Ativa);
}
Ver resposta

✓ Resposta: O teste depende do relógio real, e há pelo menos dois problemas. DateTime.Now é lido duas vezes na mesma expressão, e o valor pode mudar entre as leituras. E, se o teste rodar exatamente na virada da meia-noite, DateTime.Today, calculado na preparação, pode pertencer ao dia anterior ao do DateTime.Now lido depois, ou o fuso horário da máquina de integração pode diferir do da máquina de desenvolvimento. O resultado é um teste intermitente, que falha "às vezes" e ensina a equipe a ignorar falhas. A correção torna o tempo uma dependência explícita:

public sealed class Promocao(DateTimeOffset inicio, DateTimeOffset fim, TimeProvider relogio)
{
    public bool Ativa
    {
        get
        {
            DateTimeOffset agora = relogio.GetUtcNow();   // uma única leitura
            return agora >= inicio && agora <= fim;
        }
    }
}

[Fact]
public void Ativa_DentroDoPeriodo()
{
    var agora = new DateTimeOffset(2026, 9, 15, 12, 0, 0, TimeSpan.Zero);
    var relogio = new FakeTimeProvider(agora);
    var promocao = new Promocao(agora.AddDays(-1), agora.AddDays(30), relogio);

    Assert.True(promocao.Ativa);
}

Agora o teste é determinístico: dá o mesmo resultado em qualquer máquina, dia e fuso. E fica fácil acrescentar os testes que faltavam, como "antes do início" e "depois do fim", avançando o relógio falso. DateTimeOffset em UTC, no lugar de DateTime local, elimina ainda a ambiguidade de fuso horário.

Exercício 5

Um projeto tem 100% de cobertura de código e mesmo assim um bug grave chegou à produção. Mostre um exemplo de teste que cobre uma linha sem verificá-la e explique como detectar esse tipo de teste.

Ver resposta

✓ Resposta:

[Fact]
public void Calcular_Funciona()
{
    var politica = new DescontoPorVolume();
    politica.Calcular(100m, 5);   // executa todas as linhas... e não verifica nada
}

Esse teste executa cada linha de Calcular, e a ferramenta de cobertura as marca como cobertas. Mas não há nenhum Assert: se o percentual estivesse errado, ou se o método devolvesse zero sempre, o teste continuaria passando. A cobertura mede o que foi executado, e não o que foi verificado. Formas de detectar o problema: revisão de código atenta a testes sem verificações (alguns analisadores de xUnit alertam sobre isso); e, de forma sistemática, testes de mutação. Ferramentas como o Stryker.NET fazem pequenas alterações no código de produção, como trocar >= por >, 0.10m por 0.11m ou apagar uma linha, e rodam os testes para cada alteração. Se nenhum teste falhar, a mutação "sobreviveu", o que revela um trecho coberto, mas não verificado. O teste acima deixaria todas as mutações de Calcular sobreviverem. A lição é tratar a cobertura como um alarme (código com 0% certamente não está protegido), e nunca como um atestado de qualidade.

Comentários

Mais em Linguagem C#

EF Core: Relacionamentos e Migrations
EF Core: Relacionamentos e Migrations

Autores e pedidos como entidades relacionadas no EF Core: um para muitos com…

Nullable Reference Types: o Fim Anunciado do NullReferenceException
Nullable Reference Types: o Fim Anunciado do NullReferenceException

Tipos de referência anuláveis no C#: string e string?, a análise de fluxo que…

Autenticação e autorização com JWT: quem é você e o que pode fazer
Autenticação e autorização com JWT: quem é você e o que pode fazer

Quem pode fazer o quê na API da Livraria: 401 contra 403, a anatomia do JWT…