[C#] Introdução ao teste unitário
Teste unitário é um estilo de teste automatizado para assegurar que um trecho de código está produzindo os resultados esperados. Testes unitários podem ser usados em qualquer processo de desenvolvimento, e está na base do test-driven development.
Um teste unitário não deve consumir recursos externos, como banco de dados, APIs ou disco rígido - por definição, um teste que valida múltiplas partes trabalhando em conjunto é chamado de teste de integração.
Estrutura
Existem múltiplos cenários e estratégias diferentes na escrita de testes unitários. Abaixo estão algumas implementações básicas usando a biblioteca NUnit.
Exemplo 1 - Testando o output
O método executado deve retornar um output consistente com os parâmetros. Por exemplo, um extension method StandardDeviation deve calcular o desvio padrão de um conjunto de valores.
[Test]
public void StandardDeviation_CorrectValue()
{
// Arrange
var values = new int[] { 1, 1, 1, 2 };
// Act
var result = values.StandardDeviation();
// Assert
Assert.AreEqual(0.5, result);
}
O teste escrito usa números decimais simples, mas em outros cenários pode ser necessário aplicar uma tolerância para o ponto flutuante.
Exemplo 2 - Testando o estado
O método executado pode alterar algum estado. Por exemplo, a classe DistinctList permite adicionar valores, ignorando valores duplicados, e todas as inclusões válidas alteram a contagem de elementos.
[TestCase(new int[] { }, 0)]
[TestCase(new int[] { 0 }, 1)]
[TestCase(new int[] { 0, 0 }, 1)]
[TestCase(new int[] { 0, 1 }, 2)]
public void DistinctList_AddDuplicated_ChangesCount(int[] values, int expectedCount)
{
// Arrange
var list = new DistinctList<int>();
// Act
foreach(var value in values)
list.Add(value);
// Assert
var count = list.Count();
Assert.AreEqual(expectedCount, count);
}
Este teste verifica o valor de Count em diferentes cenários: quando não ocorre inserção, quando ocorre apenas uma inserção, quando ocorre inserção de valor duplicado e quando ocorre inserção de múltiplos valores.
Exemplo 3 - Testando chamada de método
Certas funcionalidades só devem ser disparadas em determinadas condições. Por exemplo, um AuthenticationService só deve enviar um e-mail quando o usuário possuir um endereço de e-mail.
[TestCase(null, 0)]
[TestCase("test@test.com", 1)]
public void DistinctList_AddDuplicated_ChangesCount(string? email, int emailsSent)
{
// Arrange
var mockService = new EmailServiceCount();
var user = new User(email);
IEmailService emailService = mockService;
var authService = new AuthenticationService(emailService);
// Act
authService.Approve(user);
// Assert
Assert.AreEqual(emailsSent, mockService.EmailsSent);
}
Neste cenário, foi usado um mock do EmailService. Neste mock, o Send apenas contabiliza quantas vezes a função foi chamada, sem tentar enviar um e-mail. Mocking pode ser feito manualmente, como no trecho abaixo, ou usando uma biblioteca de mocking como o moq.
public class EmailServiceCount : IEmailService
{
public int EmailsSent { get; private set; } = 0;
public void Send(MailMessage email)
{
EmailsSent++;
}
}
Criação e manutenção
Bibliotecas de teste automatizados costumam ser gratuítas, mas é necessário tempo e esforço dos desenvolvedores para escrever e realizar manutenção nestes testes.
Porém, implementar testes normalmente costumam valer a pena, pois é possível detectar bugs mais rápido, garantir que novas funcionalidades sejam implementadas corretamente, expor erros quando ocorrem modificações em funcionalidades existentes, e no geral deixam o software mais previsível e confiável.
Teste automatizado não é um recurso que necessariamente precisa substituir testes manuais ou cobrir 100% do código fonte. É possível se beneficiar dele mesmo que seja implementado apenas nas funcionalidades principais, o que já ajuda a otimizar a garantia de qualidade do software.
Em comparação com testes manuais, testes automatizados podem ser reexecutados rapidamente a qualquer momento, deixando o processo geral de testes mais enxuto, barato e rápido, e permitindo uma validação constante dentro de um processo de continuous delivery.
Testabilidade de código
Teste unitário é uma das formas mais baratas de teste automatizado, mas mesmo que a maioria das funções possam ser testadas via automação, nem todos os estilos de código são bons para a escrita de testes unitários, pois alguns estilos tornam a escrita dos testes difícil, custosa ou impossível. Por exemplo:
Felizmente, a maioria dos códigos podem ser refatorados para melhorar a testabilidade do código e permitir a criação de testes unitários. Códigos com alta testabilidade implicam em funções menores e mais granulares, que são mais fáceis de entender, reutilizar, estender e modificar.