Como você verificou, até agora, se seu código funcionava? Provavelmente da forma mais frágil possível: rodando o programa, digitando algumas entradas na mão, olhando a saída, e concluindo "parece certo". Esse método tem falhas graves. Ele não escala — a cada mudança, você teria de reverificar tudo manualmente. Ele esquece casos — você testa o que lembra, e o que esquece fica sem verificação. E ele é subjetivo — "parece certo" não é "está certo". Existe uma prática que resolve tudo isso e que é uma das que mais separam o programador amador do profissional: os testes automatizados. Um teste automatizado é um pedaço de código que verifica outro código, automaticamente e de forma repetível. Nesta aula, você aprenderá a escrevê-los, e descobrirá que eles fazem mais do que apontar erros: eles dão a você a confiança de modificar seus programas sem medo. É uma rede de segurança que muda a forma como se trabalha.
Por que testar automaticamente
O valor central de um teste automatizado não é apenas provar que o código funciona hoje — é a confiança para mudar amanhã. Sem testes, cada alteração no código é um ato de fé: você muda uma parte e reza para não ter quebrado outra, sem uma forma rápida de saber. Com uma boa coleção de testes, você muda o código, roda os testes, e em segundos sabe se algo parou de funcionar. Isso transforma o trabalho: você passa a modificar, melhorar e corrigir seus programas sem medo, porque tem uma rede de segurança que avisa imediatamente se você quebrou algo. Refatorar (que vimos na aula anterior) torna-se seguro; adicionar recursos torna-se tranquilo; corrigir um bug ganha a garantia de que ele não voltará silenciosamente. Os testes são o que permite mexer no código com segurança em vez de paralisia. E há um bônus: um bom teste é também documentação viva — ele mostra, com precisão, como o código deve se comportar, de uma forma que não pode ficar desatualizada sem que o teste falhe e o denuncie.
Preparando o terreno: um projeto de teste
Testes vivem, por convenção, num projeto separado do código que testam. Existem ferramentas prontas — chamadas frameworks de teste — que facilitam escrever e executar testes; a mais popular no mundo C# chama-se xUnit. Criamos um projeto de teste com o dotnet (a ferramenta da Aula #44), usando o modelo apropriado:
dotnet new xunit -n MeusTestes # cria um projeto de testes com o xUnit
Esse projeto de teste referencia o projeto que ele vai testar, e nele escrevemos os testes. Não se preocupe com os detalhes de configuração agora; o essencial é entender o que é um teste e como ele verifica o código. Vamos supor que temos uma classe simples a testar, uma calculadora:
public class Calculadora
{
public int Somar(int a, int b) => a + b;
public int Dividir(int a, int b) => a / b;
}
A anatomia de um teste
Um teste é um método que executa uma parte do código e verifica se o resultado é o esperado. Ele segue uma estrutura clara de três passos, fácil de memorizar: preparar (montar o cenário), executar (rodar o que se quer testar) e verificar (conferir o resultado). Veja um teste da nossa calculadora:
using Xunit;
public class CalculadoraTestes
{
[Fact] // [Fact] marca este método como um teste a ser executado
public void Somar_DoisMaisTres_RetornaCinco()
{
// Preparar: monta o cenário
var calc = new Calculadora();
// Executar: roda a operação a testar
int resultado = calc.Somar(2, 3);
// Verificar: confere se o resultado é o esperado
Assert.Equal(5, resultado);
}
}
Há três elementos novos. O [Fact] (em inglês, "fato") é uma marca que diz ao xUnit "este método é um teste; execute-o". A classe Assert (em inglês, "afirmar") faz as verificações: Assert.Equal(5, resultado) afirma que o resultado deve ser igual a 5 — se for, o teste passa; se não for, o teste falha, apontando a discrepância. E o nome do método segue uma convenção descritiva que funciona como uma frase: "Somar, dois mais três, retorna cinco" — descrevendo o que se testa, em que situação, e qual o resultado esperado. Esse nome vira a mensagem que você vê quando o teste passa ou falha, então capriche nele: um bom nome de teste é uma pequena documentação do comportamento esperado.
Executando os testes
Com os testes escritos, executá-los é um único comando:
dotnet test # encontra e executa todos os testes do projeto
O xUnit encontra todos os métodos marcados com [Fact], executa cada um, e reporta quantos passaram e quantos falharam. Um teste que passa é silencioso e "verde"; um que falha "grita", mostrando exatamente qual teste quebrou, o que era esperado, e o que foi obtido. Rodar dotnet test após cada mudança é o ritual que mantém o programa saudável: em segundos, você sabe se tudo continua funcionando. Se um teste que antes passava começa a falhar, você acabou de ser avisado, na hora, de que sua última mudança quebrou algo — e sabe exatamente o quê, antes que o problema chegue a um usuário.
Testando também o que deve dar errado
Um bom conjunto de testes verifica não só os casos de sucesso, mas também os de erro — que o código falha corretamente quando deve. Lembra que a Dividir da calculadora quebra ao dividir por zero (a exceção da Aula #40)? Podemos testar que essa exceção realmente acontece:
[Fact]
public void Dividir_PorZero_LancaExcecao()
{
var calc = new Calculadora();
// Verifica que dividir por zero LANÇA a exceção esperada:
Assert.Throws<DivideByZeroException>(() => calc.Dividir(10, 0));
}
O Assert.Throws verifica que a operação lança a exceção esperada — que o código falha da forma certa. Testar os caminhos de erro é tão importante quanto testar os de sucesso: um programa robusto é aquele cujas falhas também são previsíveis e verificadas. Cobrir bem os casos-limite (zero, valores negativos, entradas vazias, o máximo possível) é onde os testes mais frequentemente pegam bugs, pois são justamente esses casos "de canto" que os programadores esquecem ao escrever o código.
Por que isto vale o esforço
Serei honesto: escrever testes dá trabalho, e o iniciante muitas vezes o vê como tempo "perdido" que poderia ser gasto escrevendo o programa em si. Essa é uma percepção compreensível, mas equivocada, e vale desfazê-la. O tempo investido em testes retorna multiplicado, de várias formas. Eles pegam bugs cedo, quando são baratos de corrigir, em vez de tarde, quando já causaram dano. Eles dão a confiança para modificar o código sem medo, o que acelera todo o trabalho futuro. Eles impedem que bugs corrigidos voltem (basta um teste que capture o bug). E eles documentam o comportamento esperado de forma sempre atualizada. Programadores experientes não escrevem testes por disciplina imposta, mas porque descobriram, na prática, que testar os torna mais rápidos e seguros, não mais lentos. Você não precisa testar tudo exaustivamente desde o primeiro dia — mas adotar o hábito de testar ao menos a lógica importante dos seus programas é um dos investimentos mais valiosos que você pode fazer na sua qualidade como programador.
A confiança para mudar código passou a ter uma base concreta. Um teste automatizado verifica sozinho se determinado comportamento continua correto, e uma suíte de testes transforma alterações arriscadas em alterações verificáveis: mudou algo, rodou os testes, e o que quebrou aparece imediatamente em vez de chegar ao usuário.
A anatomia ficou clara — preparar a situação, executar a operação, conferir o resultado — e vale reter que testar o caminho que deve dar errado é tão importante quanto testar o que deve dar certo. O custo de escrever testes é real e aparece no início; o retorno aparece em cada alteração posterior, e cresce com a vida do projeto.
Fontes e leituras recomendadas
- Testes unitários em .NET — o panorama oficial de testes no .NET; a base desta aula.
- Testes com xUnit — o tutorial oficial de criação e execução de testes com xUnit.
- Boas práticas de testes unitários — a estrutura preparar-executar-verificar e a nomenclatura de testes.
- A estrutura Fact e Assert — a documentação do xUnit sobre marcar e verificar testes.
- Testando exceções — como verificar que o código lança as exceções esperadas.
Exercícios
Exercício 1
Crie uma classe Calculadora com os métodos Somar, Subtrair e Multiplicar. Num projeto de teste xUnit, escreva um teste [Fact] para cada operação, seguindo a estrutura preparar-executar-verificar, e dando a cada teste um nome descritivo. Execute-os com dotnet test.
Ver resposta
✓ Resposta: Exemplo (classe e testes):
public class Calculadora
{
public int Somar(int a, int b) => a + b;
public int Subtrair(int a, int b) => a - b;
public int Multiplicar(int a, int b) => a * b;
}
public class CalculadoraTestes
{
[Fact]
public void Somar_DoisMaisTres_RetornaCinco()
{
var calc = new Calculadora(); // preparar
int r = calc.Somar(2, 3); // executar
Assert.Equal(5, r); // verificar
}
// testes análogos para Subtrair e Multiplicar
}
Cada teste segue a estrutura de três passos, e dotnet test os executa, reportando os resultados.
Exercício 2
Escreva um teste que use Assert.Throws para verificar que um método Dividir(int, int) lança a exceção esperada ao dividir por zero. Explique, com suas palavras, por que testar esse caminho de erro é tão importante quanto testar os casos de sucesso.
Ver resposta
✓ Resposta: Exemplo:
[Fact]
public void Dividir_PorZero_LancaExcecao()
{
var calc = new Calculadora();
Assert.Throws<DivideByZeroException>(() => calc.Dividir(10, 0));
}
Testar o caminho de erro é tão importante quanto testar o sucesso porque um programa robusto precisa falhar corretamente: ele deve reagir a situações inválidas (como dividir por zero) de forma previsível — lançando a exceção certa —, e não de maneira inesperada. Verificar que a exceção acontece garante que essa proteção existe e continuará existindo após futuras mudanças; sem esse teste, alguém poderia alterar o método e remover ou quebrar esse comportamento sem que nada avisasse.
Exercício 3
Faça um teste falhar de propósito — por exemplo, escreva Assert.Equal(5, calc.Somar(2, 2)) (que dá 4, não 5). Execute dotnet test, observe a saída, e descreva que informação o xUnit fornece sobre a falha e como ela ajuda a localizar o problema.
Ver resposta
✓ Resposta: Ao escrever Assert.Equal(5, calc.Somar(2, 2)) (que resulta em 4) e rodar dotnet test, o teste falha, e o xUnit reporta: o nome do teste que falhou, e — o mais útil — o valor esperado (5) versus o valor obtido (4). Com isso, você localiza imediatamente qual teste quebrou e qual foi exatamente a discrepância, sem precisar depurar às cegas. A saída aponta diretamente para o problema, tornando a correção rápida.
Exercício 4
Explique, com suas palavras, a estrutura de três passos de um teste (preparar, executar, verificar), e por que essa organização torna os testes claros e fáceis de entender. Ilustre com um exemplo do que iria em cada passo ao testar um método EhPar(int).
Ver resposta
✓ Resposta: A estrutura de três passos é: preparar (montar o cenário e os dados necessários), executar (rodar a operação que se quer testar) e verificar (conferir se o resultado é o esperado). Essa organização torna os testes claros porque cada teste conta uma pequena história linear e previsível — monta a situação, age, confere —, fácil de ler e entender. Exemplo, testando EhPar(4): no preparar, cria-se o objeto ou define-se a entrada (o número 4); no executar, chama-se EhPar(4), guardando o resultado; no verificar, afirma-se que o resultado é true (Assert.True(resultado)). Os três passos deixam explícito o que está sendo testado e qual comportamento se espera.
Exercício 5
Sem código: a aula afirma que o maior valor dos testes não é provar que o código funciona hoje, mas dar "confiança para mudar amanhã". Explique o que isso significa e como uma coleção de testes muda a forma como você se sente ao modificar um programa que já funcionava.
Ver resposta
✓ Resposta: Dar "confiança para mudar amanhã" significa que o maior benefício dos testes não é a verificação pontual de hoje, mas a garantia contínua que eles oferecem sempre que você mexer no código no futuro. Sem testes, modificar um programa que funcionava é assustador: você não sabe se sua mudança quebrou algo em outra parte, e descobrir isso é lento e incerto. Com uma coleção de testes, essa sensação muda completamente: você faz a modificação, roda os testes, e em segundos sabe se tudo continua funcionando — se algum teste falhar, você é avisado na hora, e sabe exatamente o que quebrou. Isso remove o medo de mexer no código, permitindo refatorar, melhorar e adicionar recursos com tranquilidade, em vez de evitar mudanças por receio de quebrar o que já funcionava. Os testes transformam a modificação de um ato arriscado num ato seguro.