Fundamentos dos Testes de Unidade
Testes de unidade são a base para garantir que cada parte do seu código faz o que deveria, e ter domínio dos seus fundamentos permite que você transite por diversas tecnologias e linguagens com segurança. Neste artigo, vou reunir os principais fundamentos que você precisa para ser fera em testes.
Vamos passar por código testável, pela escolha do que testar, pelo reuso de código já testado, pelo ciclo Red, Green, Refactor e pela cultura de testes. Além disso, você vai ver exemplos práticos em C# e as boas práticas que a documentação oficial do .NET recomenda.
O que são testes de unidade?
Um teste de unidade exercita um pedaço pequeno do sistema, normalmente um método ou uma classe, de forma isolada. A ideia é verificar se aquela unidade se comporta como esperado diante de uma entrada conhecida.
Segundo a documentação da Microsoft, testes de unidade devem testar apenas o código que está sob o controle do desenvolvedor. Ou seja, eles não envolvem banco de dados, sistema de arquivos nem chamadas de rede, que ficam para os testes de integração.
Na prática, isso traz um benefício enorme. Como não dependem de infraestrutura, esses testes rodam em milissegundos e podem ser executados centenas de vezes por dia.
Características de um bom teste
O guia Unit testing best practices for .NET lista cinco características que definem um bom teste de unidade. Elas valem para qualquer linguagem, não só para o .NET.
- Rápido: projetos maduros têm milhares de testes, então cada um precisa rodar em milissegundos.
- Isolado: o teste roda sozinho, sem depender de fatores externos.
- Repetível: o resultado é sempre o mesmo se nada mudar entre as execuções.
- Autoverificável: o próprio teste detecta se passou ou falhou, sem intervenção humana.
- Oportuno: escrever o teste não pode levar muito mais tempo do que escrever o código testado.
Esse último ponto conversa diretamente com o próximo fundamento. Afinal, quando testar demora demais, quase sempre o problema está no design do código.
Código testável é a base dos testes de unidade
Seja em programação orientada a objetos ou em programação funcional, escrever um bom código é a base para executar testes de unidade com facilidade. Não existe atalho aqui.
O uso de SOLID e Clean Code na orientação a objetos, ou de funções puras e outras boas práticas no JavaScript, é indispensável para criar um ambiente propício a testes. Por exemplo, uma função pura sempre devolve o mesmo resultado para a mesma entrada, o que a torna trivial de testar.
Bons códigos são testados mais facilmente, tomam menos tempo e dão menos trabalho. Logo, os testes ficam muito mais simples de escrever e de manter.
Dependências explícitas
Um dos maiores inimigos dos testes é a dependência escondida. Quando uma classe cria por conta própria o acesso ao banco ou lê a data do sistema, você não consegue controlar esse comportamento no teste.
Por isso, a documentação do .NET recomenda seguir o princípio das dependências explícitas e usar injeção de dependência. Assim, no teste, você troca a implementação real por uma versão falsa e controlada.
public class OrderService
{
private readonly IOrderRepository _repository;
// A dependência chega pelo construtor e pode ser trocada no teste
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public decimal CalculateTotal(Order order)
{
var total = order.Items.Sum(item => item.Price * item.Quantity);
// Pedidos acima de 500 recebem 10% de desconto
return total > 500 ? total * 0.9m : total;
}
}
Data e hora são um caso clássico desse problema. Se quiser ver como resolver isso na prática, leia o artigo sobre como testar data e hora em .NET.
Em resumo, se você quer chegar a uma ótima cobertura de testes, passe primeiro pela base da linguagem que utiliza. Padrões, boas práticas e código limpo vêm antes.
Saiba o que testar
Se você tentar testar tudo na sua aplicação, talvez desanime e abandone os testes de unidade. Digo isso porque, eventualmente, vejo testes sobre entidades anêmicas ou validações simples, como verificar se um nome aceita X caracteres.
Para fins de aprendizado, acho excelente, porque quanto mais você exercita os testes, melhor. Mas, se não está sobrando tempo, foque no que realmente importa, como os cálculos da aplicação e as validações de fluxos mais complexos.
Se sobrar tempo, teste o resto! Quanto mais testes, mais coberto você está, mas saber de fato o que precisa ser testado é o que faz diferença.
O domínio no centro dos testes
Embora essa seja uma decisão mais de negócio, é quase intuitivo que os testes fiquem sobre as entidades do domínio. Afinal, as regras de negócio moram lá.
Apenas tome cuidado para não trazer entidades irrelevantes para o domínio. Muitas delas poderiam ser apenas um CRUD e não justificam um esforço grande de testes.
Pense, por exemplo, em um pedido com regra de desconto. Esse cálculo merece testes de unidade, enquanto um cadastro simples de categorias provavelmente não.
Cobertura não é sinônimo de qualidade
A própria documentação da Microsoft faz um alerta importante. Um percentual alto de cobertura de código não indica sucesso nem garante qualidade, porque ele apenas mostra quanto do código foi executado pelos testes.
Além disso, metas muito ambiciosas podem ser contraproducentes. Sair de 90% para 95% em um projeto complexo pode exigir um esforço enorme para cobrir casos extremos com pouco retorno.
Portanto, use a cobertura como um indicador, e não como objetivo. O que importa é que as regras críticas estejam protegidas.
Compartilhamento e reuso de código
Quantas vezes você já testou se um e-mail era válido? E se um CPF era válido?
Pois é! Esse tipo de teste se repete em quase todo projeto.
No .NET, temos o NuGet, e no JavaScript, o NPM. Os dois são ótimos gerenciadores de pacotes, e você pode ter um feed privado para a sua empresa.
Sempre que falamos em Primitive Obsession e Value Objects, falamos em compartilhar exatamente esse tipo de código. Então, quantos Value Objects de e-mail, CPF e afins você já criou, e quantos deles você compartilhou?
Menos código repetido, menos testes de unidade
Compartilhamento e reuso de código afetam diretamente os testes de unidade. Quanto mais você reusa, menos testes precisa escrever.
Sigo esse princípio com o Flunt, um pacote de validações que já vem testado. Dessa forma, não preciso repetir os testes simples, como formato de e-mail, porque o próprio pacote já os garante.
O mesmo acontece com o Entity Framework e o NHibernate. Não testamos seus contextos nem nada interno deles, pois os próprios frameworks já garantem que isso funciona.
Pense desta forma: crie, teste e distribua. Com isso, seus testes de unidade podem cair pela metade.
Red, Green, Refactor nos testes de unidade
Sabendo o que testar, escreva todos os testes e faça-os falhar. Você vai perceber que, enquanto escreve o título dos testes, surgem novas ideias e novos cenários a cobrir.
Com certa frequência, enquanto estou desenvolvendo o domínio, me vêm alguns testes na cabeça. Nesses momentos, sempre os anoto no projeto de testes de unidade.
Minha recomendação é simples. Sempre que lembrar de algo que precisa ser testado, vá até os testes daquela entidade, anote o título do teste e faça-o falhar.
Fase red: testes falhando
Ao terminar a escrita do domínio e dos testes, você terá uma gama de testes, todos vermelhos. Segundo o DevIQ, ver o teste falhar antes de passar confirma que ele realmente testa o comportamento pretendido.
public partial class OrderServiceTests
{
[Fact]
public void CalculateTotal_OrderAbove500_AppliesTenPercentDiscount()
{
// Arrange
var service = CreateOrderService();
var order = CreateOrder(price: 600m, quantity: 1);
// Act
var total = service.CalculateTotal(order);
// Assert
Assert.Equal(540m, total);
}
[Fact]
public void CalculateTotal_OrderBelow500_KeepsOriginalTotal()
{
var service = CreateOrderService();
var order = CreateOrder(price: 100m, quantity: 2);
var total = service.CalculateTotal(order);
Assert.Equal(200m, total);
}
}
Repare no nome de cada teste. A Microsoft recomenda que ele tenha três partes: o método testado, o cenário e o comportamento esperado.
Fase green: tudo passando
O próximo passo é fazer os testes passarem com o mínimo de esforço possível. Nada de antecipar necessidades futuras ou generalizar demais nesse momento.
Deixe tudo verde, com todos os testes passando. Em seguida, você vai notar que há uma certa bagunça no código, porque escrever testes também exige estruturação.
Se simplesmente deixarmos essa bagunça acumular, uma hora vamos pagar por isso. Afinal, ela pode influenciar os próprios testes e torná-los frágeis.
Fase refactor: organizando o código
Passamos, então, para o Refactor. Aqui, refatoramos os testes para reutilizar a massa de dados em vários cenários, criar repositórios falsos e por aí vai.
public partial class OrderServiceTests
{
// Métodos auxiliares reaproveitados por vários testes
private static OrderService CreateOrderService()
{
return new OrderService(new FakeOrderRepository());
}
private static Order CreateOrder(decimal price, int quantity)
{
var order = new Order();
order.AddItem(new OrderItem(price, quantity));
return order;
}
}
Note que usamos métodos auxiliares em vez de um setup compartilhado. A documentação do .NET prefere essa abordagem, porque cada teste deixa visível tudo o que usa e não compartilha estado com os outros.
Além disso, o FakeOrderRepository substitui o acesso real ao banco. Assim, o teste continua rápido, isolado e repetível.
Essa é uma das técnicas de que mais gosto na escrita de testes de unidade. Ela nos dá uma visão clara do que precisamos testar e, ao mesmo tempo, a obrigação de criar testes bem escritos.
Boas práticas na escrita de testes de unidade
Além dos fundamentos acima, algumas práticas tornam os testes de unidade mais legíveis e confiáveis. Todas elas aparecem no guia oficial de boas práticas do .NET.
- Siga o padrão Arrange, Act, Assert, separando a preparação, a execução e a verificação.
- Use a entrada mais simples possível para o comportamento testado.
- Evite strings mágicas e guarde valores relevantes em constantes com nomes claros.
- Evite lógica dentro do teste, como
if,foreswitch, porque ela pode esconder bugs. - Tenha apenas uma ação por teste, para que a falha aponte exatamente o problema.
O ponto sobre lógica merece destaque. Se um teste tem condicionais, você precisaria de outro teste para testar o teste, e aí ele deixa de inspirar confiança.
Quando vários cenários diferem apenas nos dados, use testes parametrizados. No xUnit, por exemplo, isso é feito com [Theory] e [InlineData], o que evita repetir o mesmo corpo de teste.
Se quiser ir além e medir a qualidade dos seus testes, vale conhecer os testes de mutação com Stryker no .NET. Eles mostram se os testes realmente detectam mudanças no código.
Fakes, stubs e mocks nos testes de unidade
Na fase de refatoração, citei os repositórios falsos. Eles fazem parte de uma família de objetos que substituem dependências reais durante os testes de unidade.
O nome genérico para esses substitutos costuma ser test double, ou dublê de teste. Dentro dessa família, três termos aparecem com mais frequência no dia a dia.
- Fake: uma implementação funcional e simplificada, como um repositório que guarda dados em uma lista na memória.
- Stub: um objeto que apenas devolve respostas prontas para as chamadas feitas pelo teste.
- Mock: um objeto que, além de responder, permite verificar se determinada chamada aconteceu.
public class FakeOrderRepository : IOrderRepository
{
// Armazena os pedidos em memória, sem banco de dados
private readonly List<Order> _orders = new();
public void Save(Order order) => _orders.Add(order);
public Order? GetById(Guid id) => _orders.FirstOrDefault(o => o.Id == id);
}
Na prática, prefira o dublê mais simples que resolve o cenário. Um fake escrito à mão costuma ser suficiente e deixa o teste fácil de ler.
Por outro lado, abusar de mocks pode acoplar o teste à implementação. Nesse caso, qualquer refatoração interna quebra os testes, mesmo quando o comportamento continua correto.
Cultura de testes
Já comentei sobre cultura de testes em outros artigos aqui no balta.io e reforço a importância dela aqui. Ter uma visão única sobre o valor dos testes, na empresa como um todo, é fundamental para ter códigos testáveis.
Sinceramente, não adianta a equipe ter os melhores devs e escrever os melhores testes se, na hora em que o prazo aperta, os testes são deixados de lado. A cultura existe justamente para evitar isso.
Se você já ouviu a frase "não tenho tempo para testar", vale ler o artigo Eu não tenho tempo para testar. Ele trata exatamente desse dilema do dia a dia.
Benefícios que sustentam a cultura
A documentação da Microsoft lista benefícios que ajudam a convencer o time. Os testes de unidade reduzem o tempo gasto com testes funcionais manuais, que são caros e precisam ser repetidos a cada mudança.
Além disso, eles protegem contra regressões, porque você roda a suíte inteira a cada build. Também funcionam como documentação executável, já que cada teste bem nomeado explica a saída esperada para uma entrada.
Por fim, eles incentivam um código menos acoplado. Como é difícil testar código muito acoplado, escrever testes empurra o design naturalmente para o desacoplamento.
Resumo dos fundamentos dos testes de unidade
Escreva códigos testáveis, saiba bem o que testar e reuse e distribua seu código já testado. Dessa forma, você otimiza seus testes de unidade e gasta energia onde ela realmente importa.
Utilize técnicas como o Red, Green, Refactor e siga as boas práticas de nomeação e organização. Principalmente, semeie a cultura de testes na sua empresa, porque é ela que mantém tudo de pé quando o prazo aperta.
FAQ
Qual a diferença entre teste de unidade e teste de integração?
O teste de unidade verifica uma parte isolada do código, sem infraestrutura. Já o teste de integração verifica se dois ou mais componentes funcionam juntos, muitas vezes com banco de dados ou rede.
Preciso testar todas as classes do sistema?
Não. Priorize regras de negócio, cálculos e fluxos complexos, e deixe validações triviais e CRUDs simples para depois.
Cobertura de 100% garante um código sem bugs?
Não. A cobertura mostra apenas quanto código foi executado pelos testes, e não se as verificações feitas são boas.
O que é o ciclo Red, Green, Refactor?
É o ciclo do TDD: escrever um teste que falha, fazê-lo passar da forma mais simples e depois melhorar o código sem mudar o comportamento.
Como nomear um teste de unidade?
Use três partes: o método testado, o cenário e o resultado esperado, como em CalculateTotal_OrderAbove500_AppliesTenPercentDiscount.