Aguiar Dev
Testes unitários em C# com xUnit: do primeiro teste à cobertura completa
dotnet·12 de junho de 2026

Testes unitários em C# com xUnit: do primeiro teste à cobertura completa

Aprender testes unitários é um divisor de águas na carreira de qualquer programador. Tem muita coisa que você talvez já saiba como fazer, mas sem entender de verdade o porquê. Não é sobre burocracia. É sobre confiança. Confiança para refatorar, confiança para adicionar funcionalidade, confiança para fazer deploy numa sexta à tarde.

Este artigo consolida três etapas do aprendizado:

  • escrever o primeiro teste

  • medir cobertura

  • reaproveitar casos de teste

O mundo dos testes é bem grande, mas os principais são: testes e2e, integrados e unitários. Cada tipo de teste exige um esforço para ser implementado — e esforço significa tempo, que significa dinheiro. Quanto mais demorar pra ser feito, mais caro vai custar. Pra ilustrar isso temos a boa e velha pirâmide de testes:

Pirâmide de testes

Os testes unitários são o tipo de teste mais rápido e barato de executar, e por isso formam a base da pirâmide de testes.


O que são testes unitários

Os testes unitários validam uma unidade isolada do código (geralmente um método) sem depender de banco de dados, APIs externas ou qualquer outro recurso fora do processo.

Alguns fatos importantes antes de começar:

  • Testes aumentam a qualidade do código, mas não garantem ausência de bugs. Aliás, nenhum teste garante isso.
  • Geralmente vai haver mais código de teste do que o próprio código que está sendo testado. E tá tudo bem. Isso é normal.
  • Softwares escritos sem pensar em testabilidade vão exigir refatoração para serem testados.
  • Não é necessário testar 100% do sistema; cubra o que tem lógica e impacto real.
  • Escrever testes obriga você a pensar em design — é aqui que SOLID começa a fazer sentido na prática. Se você quiser saber mais sobre isso, assista a esse vídeo.

Biblioteca de teste

A escolha de uma biblioteca de testes vai além da sintaxe dos atributos. Cada opção possui características próprias em relação à filosofia de desenvolvimento, recursos disponíveis, integração com ferramentas e adoção pelo mercado. A tabela abaixo apresenta um resumo dos principais pontos fortes e das possíveis limitações de cada uma, servindo como um guia rápido para comparação.

Biblioteca Ponto forte Possível desvantagem
MSTest Integração nativa com ferramentas Microsoft Menos recursos avançados
NUnit Grande quantidade de funcionalidades e atributos API mais extensa e, às vezes, mais verbosa
xUnit Simplicidade, boas práticas e ampla adoção Exige adaptação para quem vem de MSTest/NUnit
TUnit Alta performance, paralelismo e recursos modernos Ecossistema ainda pequeno e pouca adoção no mercado

Para projetos .NET criados atualmente, a tendência do mercado é:

  1. xUnit (mais comum)
  2. NUnit
  3. MSTest
  4. TUnit (tecnologia emergente)

Para este guia usamos xUnit, a biblioteca de testes mais adotada no ecossistema .NET moderno.


Estrutura do projeto

Crie dois projetos: um com a lógica de produção e outro com os testes.

dotnet new classlib -n Calculator
dotnet new xunit -n Calculator.Tests
dotnet add Calculator.Tests/Calculator.Tests.csproj reference Calculator/Calculator.csproj

A classe a ser testada é simples. Nosso foco deve ser na estrutura dos testes, não na lógica:

public static class Calculator
{
    public static int Sum(int firstNumber, int secondNumber)
    {
        if (firstNumber < 0 || secondNumber < 0)
            return -1;

        return firstNumber + secondNumber;
    }
}

Estrutura AAA

Todo teste unitário bem escrito segue o padrão Arrange-Act-Assert:

  • Arrange: prepara os dados e o contexto
  • Act: executa a operação sendo testada
  • Assert: verifica se o resultado é o esperado

Manter essa separação visualmente (com linhas em branco entre as seções) facilita a leitura e a manutenção.

[Fact]
public void Test1()
{
    //arrange

    //act

    //assert

}

Eu gosto de usar comentários pra deixar explícito cada "região do teste". E isso é algo que você vai notar em mais algumas coisas daqui pra frente: ser explícito. Em alguns momentos vai parecer estranho, feio ou até mesmo errado se considerarmos o que aprendemos sobre lógica e convenções da linguagem, mas isso é proposital dentro do contexto de testes.


Nomenclatura: Given-When-Then

O nome do método de teste é a primeira documentação do comportamento esperado. O padrão Given-When-Then descreve:

  • Given: qual é a condição de entrada
  • When: qual operação está sendo executada
  • Then: qual é o resultado esperado

Exemplo: GivenValidNumbers_WhenSum_ThenShouldSucceed comunica tudo sem abrir o corpo do método.

Essa é uma forma de estruturar um cenário de teste de maneira clara e legível. O padrão ficou muito conhecido com o BDD (Behavior-Driven Development), mas pode ser usado em qualquer tipo de teste, inclusive testes unitários.

Duas coisas que você talvez estranhe: o tamanho do método e o uso de underscores. Ambas absurdas do ponto de vista de boas práticas e convenções, mas lembre-se do nosso contexto: testes. Nesse contexto, tá tudo bem. Estamos propositalmente sendo mais explícitos. Lembra? Acho que já deu pra entender a ideia. Não vou mais ficar te lembrando disso.


Primeiro teste com [Fact]

Geralmente criamos uma classe de teste dedicada aos testes da classe que será testada. O nome da classe de teste é igual a classe testado mais o sufixo Tests. Então nossa classe fica CalculatorTests e o método teste com a notação [Fact] indica ao xUnit que este é um método de teste.

O primeiro teste ficará dessa forma:

public class CalculatorTests
{
    [Fact(DisplayName = "Given valid numbers, when sum, then should succeed.")]
    public void GivenValidNumbers_WhenSum_ThenShouldSucceed()
    {
        // Arrange
        const int firstNumber = 3;
        const int secondNumber = 2;
        const int resultExpected = 5;

        // Act
        var resultActual = Calculator.Sum(firstNumber, secondNumber);

        // Assert
        Assert.Equal(resultExpected, resultActual);
    }
}

Na seção Arrange estamos criando constantes para evitar magic numbers. Em Act então executamos a ação que queremos testar. Guardamos o retorno da método para verificarmos, na seção Assert, se o resultado é o esperado. Nesta seção usamos a classe Assert, que é fornecida pelo xUnit para realizar verificações.

Obs 1: as verificações precisam ser feitas sempre com a classe Assert e precisa haver pelo menos uma verificação.

Obs 2: em casos mais complexos podemos usar a classe Assert para fazer mais de uma verificação, mas também não podemos exagerar. Seja sempre o mais objetivo possível. O critério de verificação precisa ser explícito. Obs 3: temos ainda a possibilidade de usar o atributo DisplayName. Com ele podemos descrever usando linguagem natural o que o método de teste faz. Seja no Visual Studio/VSCode/Terminal, vai exibir esse texto ao invés de exibir o nome do método.

Navegue até o diretório onde está o projeto de testes e execute o com o comando:

dotnet test

O teste passa, mas tem um problema: a condição com números negativos não foi testada. Nesse caso podemos dizer que não cobrimos totalmente a nossa classe com testes.

Cobertura de testes pela metade

E aqui entra o conceito de cobertura de testes.


Cobertura de testes

Cobertura de testes é uma métrica que mostra qual percentual do código foi executado durante os testes. Ter uma alta cobertura não garante qualidade, mas baixa cobertura garante pontos cegos.

Gerando o relatório

O Coverlet já vem incluído nos projetos xUnit criados com dotnet new xunit. Ele é uma biblioteca com o objetivo de medir a cobertura de código em projetos .NET. Para gerar o relatório visual precisamos executar dois comandos. Primeiro execute o camando no diretório do projeto de testes:

dotnet test --collect:"XPlat Code Coverage"

Isso cria a pasta TestResults/ e dentro dela um arquivo XML. Esse arquivo contém as métricas de cobertura do código. Cada vez que executar o comando será gerado um novo arquivo. Para transformar esse arquivo num relatório visual, instale o ReportGenerator:

dotnet tool install -g dotnet-reportgenerator-globaltool

O ReportGenerator é a ferramenta que vai usar como base o arquivo XML que geramos para criar um relatório visual em HTML. Agora vá até o diretório onde ele foi criado e execute o comando:

reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage-report" -reporttypes:Html

Como resultado teremos a geração do diretório coveragereport contendo um site estático. Abra index.html no browser. E esta será sua visão:

Relatório inicial de cobertura

Acessando o arquivo index.html aparecerá um sumário com a cobertura de testes. E podemos perceber que no card branch coverage está indicando que temos 50% de cobertura.

Interpretando o relatório

Mais abaixo temos uma lista das classes que estão disponíveis no projeto que está sendo testado e o indicador individual da cobertura de teste. Ao clicar na classe Calculator você terá esta visão:

Relatório de cobertura parcial

Você vai notar que algumas linhas do relatório tem uma cor de fundo diferente também algumas variações. A tabela abaixo explica:

Cor Significado
Verde Linha executada pelos testes
Amarelo Condicional parcialmente coberta
Vermelho Linha sem cobertura alguma
Cinza Não testável (declaração, chave, etc.)

No nosso exemplo, a linha return -1 aparece em vermelho. Nenhum teste passou um número negativo para Sum. Hora de cobrir isso.


Testes parametrizados com [Theory] e [InlineData]

Testar a condição negativa exige três cenários:

  1. primeiro parâmetro negativo
  2. segundo parâmetro negativo
  3. ambos parâmetros negativos

A primeira coisa que pensamos pra cobrir estes três cenários de teste é em criar três métodos idênticos apenas alterando os valores de entrada e na verificação. Funcionaria e teríamos a cobertura desejada, mas seria duplicação desnecessária.

A notação [Theory] resolve isso: transforma um único método em vários casos de teste, cada um alimentado por um [InlineData]:

[Theory(DisplayName = "Given invalid numbers, when sum, then should fail.")]
[InlineData(-5, 7)]
[InlineData(10, -8)]
[InlineData(-11, -3)]
public void GivenInvalidNumbers_WhenSum_ThenShouldFail(
    int firstNumber, int secondNumber)
{
    // Arrange
    const int resultExpected = -1;

    // Act
    var resultActual = Calculator.Sum(firstNumber, secondNumber);

    // Assert
    Assert.Equal(resultExpected, resultActual);
}

Ao executar dotnet test, o runner trata cada [InlineData] como um teste independente. Você vê três execuções, três resultados e um relatório de cobertura agora completamente verde.


Resultado final

Com dois métodos de teste, cobrimos todos os caminhos do código:

public class CalculatorTests
{
    [Fact(DisplayName = "Given valid numbers, when sum, then should succeed.")]
    public void GivenValidNumbers_WhenSum_ThenShouldSucceed()
    {
        const int firstNumber = 3;
        const int secondNumber = 2;
        const int resultExpected = 5;

        var resultActual = Calculator.Sum(firstNumber, secondNumber);

        Assert.Equal(resultExpected, resultActual);
    }

    [Theory(DisplayName = "Given invalid numbers, when sum, then should fail.")]
    [InlineData(-5, 7)]
    [InlineData(10, -8)]
    [InlineData(-11, -3)]
    public void GivenInvalidNumbers_WhenSum_ThenShouldFail(
        int firstNumber, int secondNumber)
    {
        const int resultExpected = -1;

        var resultActual = Calculator.Sum(firstNumber, secondNumber);

        Assert.Equal(resultExpected, resultActual);
    }
}

Conclusão

Testes unitários não são complexos por natureza. A dificuldade quase sempre vem de código que não foi escrito para ser testado. Isso gera um código com acoplamento excessivo, dependências escondidas, métodos que fazem coisas demais, entre outras coisas.

Quando você começa a escrever testes, começa a enxergar esses problemas antes que virem bug em produção. E é nesse momento que design de software deixa de ser teoria.

Gostou do assunto? A playlist de qualidade de software no meu canal aprofunda esses conceitos com mais exemplos práticos. E se quiser trocar ideia ou tirar dúvidas, a comunidade no Discord está aberta.