C# Testes (xUnit)

Toda vez que a Ana muda o boletim da escola, ela roda o programa, lança umas notas e confere a média de cabeça. Funciona, mas cansa, e um dia ela vai esquecer de conferir um caso. Um teste automatizado é um pedaço de código que faz essa conferência por ela: chama a função, compara o resultado com o esperado e diz "passou" ou "falhou". Com centenas deles, um único comando, dotnet test, confere o programa inteiro em segundos. Nesta aula você vai escrever testes com o xUnit, a biblioteca de testes mais usada no .NET.

Você vai aprender
  • Por que testar e o que é um teste de unidade.
  • A criar um projeto de testes com dotnet new xunit e ligá-lo à biblioteca que ele testa.
  • A escrever um teste com [Fact] e Assert.Equal, e a rodar com dotnet test.
  • A ler um teste que falha (Expected × Actual) e a consertar o código.
  • Um teste com vários valores ([Theory] e [InlineData]) e o teste de exceções (Assert.Throws).
  • --filter, --list-tests, os avisos xUnit2000 e xUnit1001 e a ideia do TDD.

Todas as saídas desta aula são reais, feitas com o .NET 10 SDK (10.0.112) e o xUnit 2.9.3 na máquina fictícia da Ana, na solução ~/projetos/Escola da aula Bibliotecas de Classes. O dotnet new xunit baixa pacotes do NuGet: na primeira vez, precisa de internet e pode demorar.


Por que testar?

Na aula de bibliotecas, a média do Beto, (5,0 + 7,5) ÷ 2 = 6,25, saiu 6,2. A escola arredonda para 6,3. Ninguém percebeu na hora, porque ninguém conferiu esse caso. Um teste é a anotação permanente "com as notas 5,0 e 7,5, a média tem de ser 6,3", conferida pelo computador toda vez.

Conferir à mãoTeste automatizado
Roda o programa, digita, olha a teladotnet test
Confere os casos de que você se lembraConfere todos os casos escritos, sempre
Minutos a cada mudançaSegundos
Uma mudança aqui quebra algo ali sem ninguém verO teste daquele caso falha e avisa

Um teste de unidade (em inglês, unit test) testa um pedaço pequeno do código, como um método, sem tela, sem teclado, sem internet. É por isso que as regras ficam numa biblioteca: o projeto de testes a usa como qualquer outro programa.

Todo teste tem três partes, os três A:

  1. Arrange (preparar): criar o que o teste precisa (um boletim, algumas notas).
  2. Act (agir): chamar o que está sendo testado (ler a Media).
  3. Assert (afirmar): comparar o resultado com o esperado.

1. Criar o projeto de testes

$ dotnet new xunit -o tests/Escola.Notas.Tests
O modelo "Projeto de Teste xUnit" foi criado com êxito.

Processando ações pós-criação...
Restaurando /home/ana/projetos/Escola/tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj:
  Determinando os projetos a serem restaurados...
  /home/ana/projetos/Escola/tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj restaurado (em 2,57 seg).
A restauração foi bem-sucedida.


$ dotnet sln add tests/Escola.Notas.Tests
O projeto ‘tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj’ foi adicionado à solução.
$ dotnet reference add src/Escola.Notas --project tests/Escola.Notas.Tests
A referência ‘..\..\src\Escola.Notas\Escola.Notas.csproj’ foi adicionada ao projeto.
dotnet new xunit -o tests/Escola.Notas.Tests
O modelo "Projeto de Teste xUnit". Os testes ficam na pasta tests, e o nome é o do projeto testado mais .Tests: assim, qualquer pessoa sabe o que ele testa.
dotnet sln add …
Põe o projeto de testes na solução (veja Soluções), para o dotnet test da pasta da solução encontrá-lo.
dotnet reference add src/Escola.Notas --project …Tests
Os testes usam a biblioteca, como o programa usa. O caminho tem ..\.. porque a pasta tests/Escola.Notas.Tests está dois níveis abaixo da solução.

O .csproj de testes

$ cat tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>
«… e o ProjectReference para a biblioteca, que o reference add acrescentou …»
</Project>
<IsPackable>false</IsPackable>
O dotnet pack não deve empacotar os testes.
xunit
A biblioteca de testes: os atributos [Fact] e [Theory] e a classe Assert.
Microsoft.NET.Test.Sdk e xunit.runner.visualstudio
As peças que fazem o dotnet test (e os editores) encontrarem e rodarem os testes do xUnit.
coverlet.collector
Mede a cobertura: quais linhas do código os testes executaram. É opcional.
<Using Include="Xunit" />
Um global using Xunit; para o projeto todo (veja O Arquivo .csproj). Por isso os arquivos de teste não precisam de using Xunit;.
sem <OutputType>
Um projeto de testes não é um programa: quem o roda é o dotnet test.

O modelo também criou um arquivo de exemplo, com um teste vazio:

namespace Escola.Notas.Tests;

public class UnitTest1
{
    [Fact]
    public void Test1()
    {

    }
}
$ dotnet test
  Determinando os projetos a serem restaurados...
  /home/ana/projetos/Escola/tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj restaurado (em 570 ms).
  3 de 4 projetos estão atualizados para restauração.
  Escola.Notas -> /home/ana/projetos/Escola/src/Escola.Notas/bin/Debug/net10.0/Escola.Notas.dll
  Escola.Notas.Tests -> /home/ana/projetos/Escola/tests/Escola.Notas.Tests/bin/Debug/net10.0/Escola.Notas.Tests.dll
Execução de teste para /home/ana/projetos/Escola/tests/Escola.Notas.Tests/bin/Debug/net10.0/Escola.Notas.Tests.dll (.NETCoreApp,Version=v10.0)
Versão do VSTest 18.0.2 (x64)

Iniciando execução de teste, espere...
1 arquivos de teste no total corresponderam ao padrão especificado.

Aprovado!  – Com falha:     0, Aprovado:     1, Ignorado:     0, Total:     1, Duração: 20 ms - Escola.Notas.Tests.dll (net10.0)
Escola.Notas -> … / Escola.Notas.Tests -> …
O dotnet test compilou primeiro a biblioteca e os testes (os programas da solução não foram compilados porque os testes não dependem deles).
Versão do VSTest 18.0.2
O VSTest é o "executor" de testes do .NET: ele carrega o .dll de testes e pede ao xUnit para rodar cada um.
Aprovado! – Com falha: 0, Aprovado: 1 …
O resumo: 1 teste, 1 aprovado. Um teste vazio passa, porque nada nele falhou.

2. O primeiro teste de verdade: [Fact] e Assert.Equal

A Ana apagou o UnitTest1.cs e escreveu os testes do boletim. O terceiro é o caso do Beto:

namespace Escola.Notas.Tests;

public class BoletimTests
{
    [Fact]
    public void Media_de_tres_notas_tem_uma_casa_decimal()
    {
        var boletim = new Boletim("Ana");
        boletim.Lancar(8.5m);
        boletim.Lancar(6.0m);
        boletim.Lancar(9.25m);

        Assert.Equal(7.9m, boletim.Media);
    }

    [Fact]
    public void Sem_notas_a_media_e_zero()
    {
        var boletim = new Boletim("Ana");

        Assert.Equal(0m, boletim.Media);
    }

    [Fact]
    public void Media_6_25_arredonda_para_6_3()
    {
        var boletim = new Boletim("Beto");
        boletim.Lancar(5.0m);
        boletim.Lancar(7.5m);

        Assert.Equal(6.3m, boletim.Media);
    }
}
public class BoletimTests
Uma classe de testes: costuma ter o nome da classe testada mais Tests. Ela precisa ser public.
[Fact]
"Fato": um atributo (veja Atributos) que marca o método como um teste. Sem ele, o xUnit ignora o método.
public void Media_de_tres_notas_tem_uma_casa_decimal()
O nome do teste é uma frase que diz o que deve acontecer. Quando ele falhar, só o nome já explica o problema. Os _ são só para ler melhor.
var boletim = …; boletim.Lancar(…);
Arrange: preparar.
Assert.Equal(7.9m, boletim.Media);
Act e Assert: "afirme que são iguais". O primeiro argumento é o esperado; o segundo, o que o código deu. Se forem diferentes, o teste falha ali.

Um teste que falha

$ dotnet test
  Determinando os projetos a serem restaurados...
  Todos os projetos estão atualizados para restauração.
  Escola.Notas -> /home/ana/projetos/Escola/src/Escola.Notas/bin/Debug/net10.0/Escola.Notas.dll
  Escola.Notas.Tests -> /home/ana/projetos/Escola/tests/Escola.Notas.Tests/bin/Debug/net10.0/Escola.Notas.Tests.dll
Execução de teste para /home/ana/projetos/Escola/tests/Escola.Notas.Tests/bin/Debug/net10.0/Escola.Notas.Tests.dll (.NETCoreApp,Version=v10.0)
Versão do VSTest 18.0.2 (x64)

Iniciando execução de teste, espere...
1 arquivos de teste no total corresponderam ao padrão especificado.
[xUnit.net 00:00:00.41]     Escola.Notas.Tests.BoletimTests.Media_6_25_arredonda_para_6_3 [FAIL]
  Com falha Escola.Notas.Tests.BoletimTests.Media_6_25_arredonda_para_6_3 [26 ms]
  Mensagem de erro:
   Assert.Equal() Failure: Values differ
Expected: 6,3
Actual:   6,2
  Rastreamento de pilha:
     at Escola.Notas.Tests.BoletimTests.Media_6_25_arredonda_para_6_3() in /home/ana/projetos/Escola/tests/Escola.Notas.Tests/BoletimTests.cs:line 31
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Com falha! – Com falha:     1, Aprovado:     2, Ignorado:     0, Total:     3, Duração: 113 ms - Escola.Notas.Tests.dll (net10.0)

Leia de cima para baixo:

[FAIL]
Qual teste falhou: Media_6_25_arredonda_para_6_3. O nome já diz o que se esperava.
Assert.Equal() Failure: Values differ
"Falha no Assert.Equal: os valores são diferentes."
Expected: 6,3 / Actual: 6,2
Esperado (o que você escreveu no teste) e obtido (o que o código deu). É a linha mais importante.
at … BoletimTests.cs:line 31
A linha do Assert que falhou. As duas linhas System.Reflection… são o xUnit chamando o teste: pode ignorar.
Com falha: 1, Aprovado: 2
Os outros dois testes continuaram passando. Um teste que falha não impede os outros de rodar.

O teste fez o trabalho dele: achou o defeito. O Math.Round arredonda os números "do meio" para o vizinho par (6,25 → 6,2). A Ana consertou a biblioteca (não o teste!), na linha de Regras.cs:

    public static decimal Arredondar(decimal valor) => Math.Round(valor, 1, MidpointRounding.AwayFromZero);
$ dotnet test 2>&1 | tail -2

Aprovado!  – Com falha:     0, Aprovado:     3, Ignorado:     0, Total:     3, Duração: 44 ms - Escola.Notas.Tests.dll (net10.0)

MidpointRounding.AwayFromZero ("o meio vai para longe do zero") é o arredondamento da escola: 6,25 → 6,3. Os três passam. E, a partir de agora, se alguém mexer no arredondamento, esse teste avisa.

O editor do guia não tem o xUnit, mas a ideia de um teste cabe em qualquer programa: comparar o esperado com o obtido. Experimente:

Missão: abaixo, um "testador" feito à mão confere a função Media com três casos, como os testes da Ana. Rode: um caso FALHOU. Conserte a função (não os testes) acrescentando MidpointRounding.AwayFromZero ao Math.Round, até aparecer 3 de 3 passaram.

int passaram = 0, total = 0;

Verificar(7.9m, Media([8.5m, 6.0m, 9.25m]), "média de três notas");
Verificar(0m, Media([]), "sem notas, média zero");
Verificar(6.3m, Media([5.0m, 7.5m]), "6,25 arredonda para 6,3");

Console.WriteLine($"{passaram} de {total} passaram");

void Verificar(decimal esperado, decimal obtido, string nome)
{
    total++;
    if (esperado == obtido)
    {
        passaram++;
        Console.WriteLine($"ok     {nome}");
    }
    else
    {
        Console.WriteLine($"FALHOU {nome}: esperado {esperado}, obtido {obtido}");
    }
}

static decimal Media(decimal[] notas) =>
    notas.Length == 0 ? 0 : Math.Round(notas.Average(), 1);
int passaram = 0, total = 0;

Verificar(7.9m, Media([8.5m, 6.0m, 9.25m]), "média de três notas");
Verificar(0m, Media([]), "sem notas, média zero");
Verificar(6.3m, Media([5.0m, 7.5m]), "6,25 arredonda para 6,3");

Console.WriteLine($"{passaram} de {total} passaram");

void Verificar(decimal esperado, decimal obtido, string nome)
{
    total++;
    if (esperado == obtido)
    {
        passaram++;
        Console.WriteLine($"ok     {nome}");
    }
    else
    {
        Console.WriteLine($"FALHOU {nome}: esperado {esperado}, obtido {obtido}");
    }
}

static decimal Media(decimal[] notas) =>
    notas.Length == 0 ? 0 : Math.Round(notas.Average(), 1, MidpointRounding.AwayFromZero);

3. Vários valores num teste: [Theory] e [InlineData]

Para testar a Situacao, a Ana quer conferir vários casos: 7,0 (bem no limite) aprova, 6,9 não. Em vez de um [Fact] para cada, um [Theory] ("teoria") recebe os valores como parâmetros:

public class SituacaoTests
{
    [Theory]
    [InlineData(7.0, "Aprovado")]
    [InlineData(9.5, "Aprovado")]
    [InlineData(6.9, "Recuperação")]
    [InlineData(0.0, "Recuperação")]
    public void Situacao_depende_da_media(double nota, string esperado)
    {
        var boletim = new Boletim("Ana");
        boletim.Lancar((decimal)nota);

        Assert.Equal(esperado, boletim.Situacao);
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(11)]
    public void Nota_fora_de_0_a_10_lanca_excecao(int nota)
    {
        var boletim = new Boletim("Ana");

        var erro = Assert.Throws<ArgumentOutOfRangeException>(() => boletim.Lancar(nota));
        Assert.Equal("nota", erro.ParamName);
    }
}
[Theory]
Um teste com parâmetros: ele roda uma vez para cada [InlineData].
[InlineData(7.0, "Aprovado")]
"Dados na linha": os valores de uma rodada, na ordem dos parâmetros (nota = 7.0, esperado = "Aprovado").
double nota … (decimal)nota
Um atributo não aceita valores decimal (o C# não deixa: 7.0m não pode ser argumento de atributo). Por isso o parâmetro é double, convertido para decimal dentro do teste (veja Conversão de Tipos).
Assert.Throws<ArgumentOutOfRangeException>(() => …)
"Afirme que lança": roda a lambda e passa se ela lançar essa exceção; falha se não lançar nada (ou se lançar outra). Ele devolve a exceção, para você conferir mais coisas nela.
Assert.Equal("nota", erro.ParamName)
Confere que a exceção fala do parâmetro certo.
$ dotnet test 2>&1 | tail -2

Aprovado!  – Com falha:     0, Aprovado:     9, Ignorado:     0, Total:     9, Duração: 67 ms - Escola.Notas.Tests.dll (net10.0)
$ dotnet test --list-tests 2>&1 | tail -12
Versão do VSTest 18.0.2 (x64)

Os Testes a seguir estão disponíveis:
    Escola.Notas.Tests.BoletimTests.Media_de_tres_notas_tem_uma_casa_decimal
    Escola.Notas.Tests.BoletimTests.Sem_notas_a_media_e_zero
    Escola.Notas.Tests.BoletimTests.Media_6_25_arredonda_para_6_3
    Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 7, esperado: "Aprovado")
    Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 9,5, esperado: "Aprovado")
    Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 6,9000000000000004, esperado: "Recuperação")
    Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 0, esperado: "Recuperação")
    Escola.Notas.Tests.SituacaoTests.Nota_fora_de_0_a_10_lanca_excecao(nota: -1)
    Escola.Notas.Tests.SituacaoTests.Nota_fora_de_0_a_10_lanca_excecao(nota: 11)

Nove testes: 3 [Fact] + 4 rodadas de um [Theory] + 2 do outro. O --list-tests ("liste os testes") mostra cada rodada com os seus valores. Repare no 6,9000000000000004: é o double 6.9, que não tem representação exata no computador (veja float, double e decimal). A conversão (decimal)nota o arredonda para 6,9, e o teste passa.


4. Rodar só alguns testes: --filter

$ dotnet test --filter "FullyQualifiedName~SituacaoTests" 2>&1 | tail -1
Aprovado!  – Com falha:     0, Aprovado:     6, Ignorado:     0, Total:     6, Duração: 50 ms - Escola.Notas.Tests.dll (net10.0)
--filter "FullyQualifiedName~SituacaoTests"
"Filtre": só os testes cujo nome completo (namespace + classe + método) contém (~) SituacaoTests. Com = em vez de ~, o nome tem de ser exatamente igual. Útil quando há centenas de testes e você está mexendo numa parte só.

Para ver o nome de cada teste aprovado, aumente a conversa do executor:

$ dotnet test --logger "console;verbosity=normal" 2>&1 | grep -E "Aprovado|Com falha" | head -12
  Aprovado Escola.Notas.Tests.SituacaoTests.Nota_fora_de_0_a_10_lanca_excecao(nota: 11) [9 ms]
  Aprovado Escola.Notas.Tests.SituacaoTests.Nota_fora_de_0_a_10_lanca_excecao(nota: -1) [< 1 ms]
  Aprovado Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 7, esperado: "Aprovado") [< 1 ms]
  Aprovado Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 6,9000000000000004, esperado: "Recuperação") [< 1 ms]
  Aprovado Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 9,5, esperado: "Aprovado") [< 1 ms]
  Aprovado Escola.Notas.Tests.SituacaoTests.Situacao_depende_da_media(nota: 0, esperado: "Recuperação") [< 1 ms]
  Aprovado Escola.Notas.Tests.BoletimTests.Media_6_25_arredonda_para_6_3 [22 ms]
  Aprovado Escola.Notas.Tests.BoletimTests.Media_de_tres_notas_tem_uma_casa_decimal [< 1 ms]
  Aprovado Escola.Notas.Tests.BoletimTests.Sem_notas_a_media_e_zero [< 1 ms]
     Aprovados: 9

A ordem não é a do arquivo: o xUnit não promete nenhuma ordem. Por isso, um teste nunca pode depender de outro: cada um cria o seu próprio boletim (o xUnit cria até um objeto novo da classe de testes para cada teste).


5. As verificações mais usadas

VerificaçãoPassa quando…
Assert.Equal(esperado, obtido)Os dois são iguais.
Assert.NotEqual(a, b)São diferentes.
Assert.True(condição) / Assert.False(condição)A condição é true / false.
Assert.Null(x) / Assert.NotNull(x)x é / não é null.
Assert.Contains("Ana", texto)O texto (ou a lista) contém o item.
Assert.Empty(lista) / Assert.Single(lista)A lista está vazia / tem um item só.
Assert.Throws<TipoDaExceção>(() => …)O código lança exatamente essa exceção.
await Assert.ThrowsAsync<…>(() => …)O mesmo, para métodos async (o teste vira public async Task).
Assert.InRange(valor, mínimo, máximo)O valor está no intervalo.

6. A ideia do TDD

A Ana escreveu o teste do 6,25 antes de consertar o arredondamento: primeiro ele falhou, depois ela mudou o código, depois ele passou. Fazer isso de propósito, sempre, tem nome: TDD (Test-Driven Development, "desenvolvimento guiado por testes"). O ciclo tem três passos:

  1. Vermelho: escreva um teste para o que ainda não funciona. Rode: ele falha.
  2. Verde: escreva o mínimo de código para ele passar. Rode: todos passam.
  3. Refatorar: melhore o código (nomes, repetições) com os testes passando. Se algum falhar, você quebrou algo.

Ver o teste falhar primeiro é importante: um teste que nunca falhou pode estar testando a coisa errada (ou nada).


Quando dá errado

Assert.Throws() Failure: No exception was thrown
$ dotnet test --filter "FullyQualifiedName~ErrosTests" 2>&1 | sed -n '/Iniciando/,$p'
Iniciando execução de teste, espere...
1 arquivos de teste no total corresponderam ao padrão especificado.
[xUnit.net 00:00:00.32]     Escola.Notas.Tests.ErrosTests.Nota_10_lanca_excecao [FAIL]
  Com falha Escola.Notas.Tests.ErrosTests.Nota_10_lanca_excecao [7 ms]
  Mensagem de erro:
   Assert.Throws() Failure: No exception was thrown
Expected: typeof(System.ArgumentOutOfRangeException)
  Rastreamento de pilha:
     at Escola.Notas.Tests.ErrosTests.Nota_10_lanca_excecao() in /home/ana/projetos/Escola/tests/Escola.Notas.Tests/ErrosTests.cs:line 10
«…»

Com falha! – Com falha:     1, Aprovado:     0, Ignorado:     0, Total:     1, Duração: 36 ms - Escola.Notas.Tests.dll (net10.0)

O que aconteceu: o teste esperava uma exceção em boletim.Lancar(10m), e nenhuma veio ("nenhuma exceção foi lançada"). Aqui quem está errado é o teste: 10 é uma nota válida.

Como resolver: quando um teste falha, pergunte primeiro "o que está errado: o código ou o teste?". Aqui, o teste certo usa um valor inválido, como 10.5m ou 11m.

warning xUnit2000: esperado e obtido trocados
$ dotnet test 2>&1 | grep warning | sort -u
/home/ana/projetos/Escola/tests/Escola.Notas.Tests/ErrosTests.cs(19,9): warning xUnit2000: The literal or constant value 8m should be passed as the 'expected' argument in the call to 'Assert.Equal(expected, actual)' in method 'Ordem_trocada' on type 'ErrosTests'. Swap the parameter values. (https://xunit.net/xunit.analyzers/rules/xUnit2000) [/home/ana/projetos/Escola/tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj]

O que aconteceu: a Ana escreveu Assert.Equal(boletim.Media, 8m). O teste até passa, mas, se um dia falhar, a mensagem vai dizer Expected e Actual trocados, e confundir quem ler. O xUnit vem com analisadores que conferem os testes enquanto compilam; os avisos dele começam com xUnit e vêm em inglês: "o valor literal 8m deveria ser o argumento expected… troque os valores".

Como resolver: o esperado vem primeiro: Assert.Equal(8m, boletim.Media).

error xUnit1001: Fact methods cannot have parameters.
$ dotnet test 2>&1 | grep -E "error|warning" | sort -u
/home/ana/projetos/Escola/tests/Escola.Notas.Tests/ErrosTests.cs(6,17): error xUnit1001: Fact methods cannot have parameters. Remove the parameters from the method or convert it into a Theory. (https://xunit.net/xunit.analyzers/rules/xUnit1001) [/home/ana/projetos/Escola/tests/Escola.Notas.Tests/Escola.Notas.Tests.csproj]

O que aconteceu: um método com [Fact] tinha um parâmetro (int nota). Um [Fact] não recebe valores: de onde viria a nota?

Como resolver: como a própria mensagem diz, tire o parâmetro ou troque para [Theory] com [InlineData(…)].

O teste não aparece no total

O que aconteceu: não há mensagem de erro: o teste novo simplesmente não roda. As causas comuns: faltou o [Fact] (ou [Theory]) em cima do método; o método ou a classe não é public; o projeto de testes não está na solução e você rodou dotnet test na pasta da solução.

Como resolver: dotnet test --list-tests mostra o que o xUnit encontrou. Se o teste não estiver na lista, confira o atributo e o public; se o projeto todo sumiu, dotnet sln list.


Referência rápida

Comando ou códigoO que faz
dotnet new xunit -o tests/Nome.TestsCria o projeto de testes.
dotnet reference add src/Nome --project tests/Nome.TestsLiga os testes ao que eles testam.
dotnet testCompila e roda todos os testes.
dotnet test --list-testsLista os testes encontrados.
dotnet test --filter "FullyQualifiedName~Texto"Roda só os testes com "Texto" no nome.
dotnet test --logger "console;verbosity=normal"Mostra cada teste aprovado.
[Fact]Um teste sem parâmetros.
[Theory] + [InlineData(…)]Um teste que roda com cada linha de dados.
Assert.Equal(esperado, obtido)Afirma que são iguais (o esperado primeiro).
Assert.Throws<T>(() => …)Afirma que o código lança a exceção T.

Exercícios

1. Marcar o teste

Complete o atributo de um teste sem parâmetros:

[]
public void Nota_7_aprova() { … }

2. A ordem do Assert

A média esperada é 8m. Complete na ordem certa:

Assert.Equal(, boletim.Media);

3. Rodar os testes

Complete o comando que compila e roda todos os testes da solução:

dotnet

4. O dotnet test mostrou Expected: 6,3 e Actual: 6,2. O que isso quer dizer?

5. Você quer testar a Situacao com as médias 7,0, 6,9 e 10,0. Qual é o jeito mais simples?

6. Um teste falhou. Qual é a primeira pergunta?


Resumo

  • Um teste automatizado confere um caso do seu código sempre que você roda dotnet test: preparar, agir, afirmar.
  • Projeto de testes: dotnet new xunit -o tests/Nome.Tests, dotnet sln add e dotnet reference add para a biblioteca testada.
  • [Fact] marca um teste; Assert.Equal(esperado, obtido) compara, com o esperado primeiro.
  • No teste que falha, leia o nome, o Expected, o Actual e a linha. Conserte o código (ou o teste, se ele estiver errado).
  • [Theory] + [InlineData] roda o mesmo teste com vários valores; Assert.Throws testa exceções.
  • --filter escolhe os testes; --list-tests mostra o que foi encontrado; os analisadores xUnit… avisam dos erros comuns.
  • TDD: vermelho (teste falha), verde (código passa), refatorar.