Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Há inúmeros benefícios em escrever testes de unidade. Eles ajudam com a regressão, fornecem documentação e facilitam um bom design. Mas quando os testes de unidade são difíceis de ler e frágeis, eles podem causar estragos em sua base de código. Este artigo descreve algumas práticas recomendadas para criar testes de unidade para dar suporte a seus projetos .NET Core e .NET Standard. Você aprende técnicas para manter seus testes resilientes e fáceis de entender.
Por John Reese com agradecimentos especiais a Roy Osherove
Benefícios do teste de unidade
As seções a seguir descrevem vários motivos para gravar testes de unidade para seus projetos .NET Core e .NET Standard.
Menos tempo executando testes funcionais
Testes funcionais são caros. Normalmente, eles envolvem abrir o aplicativo e executar uma série de etapas que você (ou outra pessoa) deve seguir para validar o comportamento esperado. Essas etapas podem nem sempre ser conhecidas pelo testador. Eles têm que entrar em contato com alguém mais experiente na área para realizar o teste. O teste em si pode levar segundos para alterações triviais ou minutos para alterações maiores. Por fim, esse processo deve ser repetido para cada alteração que você fizer no sistema. Os testes de unidade, por outro lado, exigem milissegundos, podem ser executados ao pressionar um botão e não necessariamente exigem qualquer conhecimento do sistema em geral. O executor de teste determina se o teste é aprovado ou falha, não o indivíduo.
Proteção contra regressão
Defeitos de regressão são erros introduzidos quando uma alteração é feita no aplicativo. É comum que os testadores não só testem seu novo recurso, mas também testem recursos que existiam com antecedência para verificar se os recursos existentes ainda funcionam conforme o esperado. Com o teste de unidade, você pode executar novamente todo o conjunto de testes após cada build ou até mesmo depois de alterar uma linha de código. Essa abordagem ajuda a aumentar a confiança de que seu novo código não interrompe a funcionalidade existente.
Documentação executável
Pode nem sempre ser óbvio o que um método específico faz ou como ele se comporta dada uma determinada entrada. Você pode se perguntar: Como esse método se comporta se eu passar uma cadeia de caracteres em branco ou nula? Quando você tem um conjunto de testes de unidade bem nomeados, cada teste deve explicar claramente a saída esperada para uma determinada entrada. Além disso, o teste deve ser capaz de verificar se ele realmente funciona.
Código menos acoplado
Quando o código está firmemente acoplado, pode ser difícil testar a unidade. Sem criar testes de unidade para o código que você está escrevendo, o acoplamento pode ser menos aparente. Escrever testes para seu código o desacoplará naturalmente, porque seria mais difícil de testá-lo.
Características de bons testes de unidade
Há várias características importantes que definem um bom teste de unidade:
- Rápido: não é incomum que projetos maduros tenham milhares de testes de unidade. Os testes de unidade devem levar pouco tempo para serem executados. Milissegundos.
- Isolado: os testes de unidade são autônomos, podem ser executados isoladamente e não têm dependências em nenhum fator externo, como um sistema de arquivos ou um banco de dados.
- Repetível: a execução de um teste de unidade deve ser consistente com seus resultados. O teste sempre retornará o mesmo resultado se você não alterar nada entre uma execução e outra.
- Autoverificação: O teste deve detectar automaticamente se passou ou falhou sem nenhuma interação humana.
- Rapidez: um teste de unidade não deve levar um tempo desproporcionalmente longo para ser escrito em comparação com o código que está sendo testado. Se você descobrir que testar o código leva muito tempo em comparação com a gravação do código, considere um design mais testável.
Cobertura de código e qualidade de código
Um alto percentual de cobertura de código geralmente está associado a uma qualidade de código mais alta. No entanto, a medida em si não pode determinar a qualidade do código. Definir uma meta percentual de cobertura de código excessivamente ambiciosa pode ser contraproducente. Considere um projeto complexo com milhares de ramificações condicionais e suponha que você defina uma meta de cobertura de código de 95%. Atualmente, o projeto mantém cobertura de código% de 90%. O tempo necessário para considerar todos os casos de borda nos 5% restantes pode ser um período enorme, e a proposta de valor diminui rapidamente.
Uma alta porcentagem de cobertura de código não é um indicador de êxito e não implica alta qualidade de código. Ele representa apenas a quantidade de código que é coberta pelos testes de unidade. Para obter mais informações, confira a cobertura de código de teste de unidade.
Terminologia de testes unitários
Vários termos são usados com frequência no contexto do teste de unidade: falso, fictício e stub. Infelizmente, esses termos podem ser mal aplicados, portanto, é importante entender o uso correto.
Falso: Um falso é um termo genérico que pode ser usado para descrever um stub ou um objeto fictício. Se o objeto é um stub ou uma simulação depende do contexto no qual o objeto é usado. Ou seja, uma falsificação pode ser um stub ou um objeto fictício.
Simulação: um objeto fictício é um objeto falso no sistema que decide se um teste de unidade é aprovado ou não. Um objeto fictício começa como falso e permanece falso até entrar em uma operação
Assert
.Stub: Um stub é uma substituição controlada para uma dependência existente (ou colaborador) do sistema. Usando um stub, você pode testar seu código sem lidar diretamente com a dependência. Por padrão, um stub começa como falso.
Considere o seguinte código:
var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Esse código mostra um stub chamado de objeto fictício. Mas, nesse cenário, o stub é realmente um stub. A finalidade do código é passar a ordem como um meio de instanciar o Purchase
objeto (o sistema em teste). O nome da classe MockOrder
é enganoso porque a ordem é um stub e não um objeto fictício.
O código a seguir mostra um design mais preciso:
var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Quando a classe é renomeada para FakeOrder
, a classe se torna mais genérica. A classe pode ser usada como uma simulação ou um stub, de acordo com os requisitos do caso de teste. No primeiro exemplo, a FakeOrder
classe é usada como um stub e não é usada durante a Assert
operação. O código passa a FakeOrder
classe para a Purchase
classe apenas para atender aos requisitos do construtor.
Para usar a classe como uma simulação, você pode atualizar o código:
var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Validated);
Nesse design, o código verifica uma propriedade na falsificação (fazendo uma afirmação em relação ela) e, portanto, a classe mockOrder
é um objeto fictício.
Importante
É importante implementar a terminologia corretamente. Se você chamar seus stubs de "simulações", outros desenvolvedores farão suposições falsas sobre sua intenção.
A principal coisa a lembrar sobre os objetos fictícios em relação aos stubs é que os objetos fictícios são como os stubs, exceto pelo processo Assert
. Você executa as operações Assert
em relação a um objeto fictício, mas não em relação a um stub.
Práticas recomendadas
Há várias práticas recomendadas importantes a seguir ao escrever testes de unidade. As seções a seguir fornecem exemplos que mostram como aplicar as práticas recomendadas ao seu código.
Evitar dependências de infraestrutura
Tente não introduzir dependências na infraestrutura ao escrever testes de unidade. As dependências tornam os testes lentos e frágeis e devem ser reservados para testes de integração. Você pode evitar essas dependências em seu aplicativo seguindo o Princípio de Dependências Explícitas e usando a injeção de dependência do .NET. Você também pode manter seus testes de unidade em um projeto separado dos testes de integração. Essa abordagem garante que seu projeto de teste de unidade não tenha referências ou dependências em pacotes de infraestrutura.
Seguir os padrões de nomenclatura para testes
O nome do teste deve consistir em três partes:
- Nome do método que está sendo testado
- Cenário no qual o método está sendo testado
- Comportamento esperado quando o cenário é invocado
Os padrões de nomenclatura são importantes porque ajudam a expressar a finalidade do teste e o aplicativo. Os testes são mais do que apenas garantir que o código funcione. Eles também fornecem documentação. Apenas examinando o conjunto de testes de unidade, você deve ser capaz de inferir o comportamento do código e não precisar examinar o próprio código. Além disso, quando os testes falham, você pode ver exatamente quais cenários não atendem às suas expectativas.
Código original
[Fact]
public void Test_Single()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Aplicar a prática recomendada
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Organizar seus testes
O padrão "Organizar, Agir, Afirmar" é uma abordagem comum para escrever testes de unidade. Como o nome indica, o padrão consiste em três tarefas principais:
- Organizar seus objetos, criá-los e configurá-los conforme necessário
- Agir em um objeto
- Afirmar que algo é o esperado
Ao seguir o padrão, você pode separar claramente o que está sendo testado das tarefas Organizar e Afirmar. O padrão também ajuda a reduzir a oportunidade de as asserções se intercalarem com o código na tarefa Act.
A legibilidade é um dos aspectos mais importantes ao escrever um teste de unidade. Separar cada ação padrão dentro do teste realça claramente as dependências necessárias para chamar seu código, como seu código é chamado e o que você está tentando afirmar. Embora seja possível combinar algumas etapas e reduzir o tamanho do teste, a meta geral é tornar o teste o mais legível possível.
Código original
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Assert
Assert.Equal(0, stringCalculator.Add(""));
}
Aplicar a prática recomendada
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add("");
// Assert
Assert.Equal(0, actual);
}
Escrever testes com o mínimo de aprovação
A entrada para um teste de unidade deve ser as informações mais simples necessárias para verificar o comportamento que você está testando no momento. A abordagem minimalista ajuda os testes a se tornarem mais resilientes a alterações futuras na base de código e se concentram em verificar o comportamento sobre a implementação.
Testes que incluem mais informações do que o necessário para passar no teste atual têm maior chance de introduzir erros no teste e podem tornar a intenção do teste menos clara. Ao escrever testes, você deseja se concentrar no comportamento. Definir propriedades extras em modelos ou usar valores diferentes de zero quando não forem necessários, apenas diminui o que você está tentando confirmar.
Código original
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("42");
Assert.Equal(42, actual);
}
Aplicar a prática recomendada
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Evitar cadeias de caracteres mágicas
As cadeias de caracteres mágicas são os valores da cadeia de caracteres codificados diretamente nos testes de unidade sem nenhum comentário ou contexto adicional de código. Esses valores tornam seu código menos legível e mais difícil de manter. Cadeias de caracteres mágicas podem gerar confusão no leitor dos seus testes. Se uma cadeia de caracteres estiver fora do comum, ela poderá se perguntar por que um determinado valor foi escolhido para um parâmetro ou valor retornado. Esse tipo de valor de cadeia de caracteres pode levá-los a dar uma olhada mais de perto nos detalhes da implementação, em vez de se concentrar no teste.
Dica
Estabeleça como meta expressar o máximo de intenção possível no código dos testes de unidade. Em vez de usar cadeias de caracteres mágicas, atribua valores embutidos em código a constantes.
Código original
[Fact]
public void Add_BigNumber_ThrowsException()
{
var stringCalculator = new StringCalculator();
Action actual = () => stringCalculator.Add("1001");
Assert.Throws<OverflowException>(actual);
}
Aplicar a prática recomendada
[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
const string MAXIMUM_RESULT = "1001";
Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
Assert.Throws<OverflowException>(actual);
}
Evite incluir lógica de codificação nos testes de unidade
Ao escrever seus testes de unidade, evite a concatenação manual de cadeias de caracteres e condições lógicas, como no caso de if
, while
, for
e switch
, além de outras condições. Se você incluir lógica em seu conjunto de testes, a chance de introduzir bugs aumentará drasticamente. O último lugar em que você deseja encontrar um bug está no conjunto de testes. Você deve ter um alto nível de confiança de que seus testes funcionam, caso contrário, você não pode confiar neles. Testes em que você não confia, não fornecem nenhum valor. Quando um teste falha, você deseja ter a sensação de que algo está errado com seu código e que ele não pode ser ignorado.
Dica
Se adicionar lógica em seu teste parecer inevitável, considere dividir o teste em dois ou mais testes diferentes para limitar os requisitos lógicos.
Código original
[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
var stringCalculator = new StringCalculator();
var expected = 0;
var testCases = new[]
{
"0,0,0",
"0,1,2",
"1,2,3"
};
foreach (var test in testCases)
{
Assert.Equal(expected, stringCalculator.Add(test));
expected += 3;
}
}
Aplicar a prática recomendada
[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add(input);
Assert.Equal(expected, actual);
}
Use métodos auxiliares em vez de Setup e Teardown
Se você precisar de um objeto ou estado semelhante para seus testes, use um método auxiliar em vez dos atributos Setup
e Teardown
, se eles existirem. Os métodos auxiliares são preferenciais em relação a esses atributos por vários motivos:
- Menos confusão ao ler os testes porque todo o código está visível de dentro de cada teste
- Menor chance de configurar demais ou de menos para o teste específico
- Menor chance de compartilhar o estado entre testes, o que cria dependências indesejadas entre eles
Nas estruturas de teste de unidade, o atributo Setup
é chamado antes de cada teste de unidade dentro do seu conjunto de testes. Alguns programadores veem esse comportamento como útil, mas geralmente resulta em testes inchados e difíceis de ler. Cada teste geralmente tem requisitos diferentes para instalação e execução. Infelizmente, o Setup
atributo força você a usar exatamente os mesmos requisitos para cada teste.
Observação
Os atributos SetUp
e TearDown
são removidos na versão 2.x e posterior do xUnit.
Código original
Aplicar a prática recomendada
private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
var stringCalculator = CreateDefaultStringCalculator();
var actual = stringCalculator.Add("0,1");
Assert.Equal(1, actual);
}
// More tests...
// More tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
var result = stringCalculator.Add("0,1");
Assert.Equal(1, result);
}
private StringCalculator CreateDefaultStringCalculator()
{
return new StringCalculator();
}
Evitar várias tarefas Act
Quando for escrever seus testes, tente incluir apenas uma tarefa Act por teste. Algumas abordagens comuns para implementar uma única tarefa Act incluem criar um teste separado para cada Act ou usar os testes parametrizados. Há vários benefícios em usar uma única tarefa de ação para cada teste.
- Você pode facilmente distinguir qual tarefa Act está falhando se o teste falhar.
- Você pode garantir que o teste esteja focado em apenas um único caso.
- Você obtém uma visão clara de por que seus testes estão falhando.
As várias tarefas Act precisam ser afirmadas individualmente, e você não pode garantir que todas as tarefas Assert sejam executadas. Na maioria das estruturas de testes de unidade, depois que uma tarefa Assert falha em um teste de unidade, todos os testes subsequentes são automaticamente considerados com falha. O processo pode ser confuso porque algumas funcionalidades de trabalho podem ser interpretadas como falha.
Código original
[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
// Act
var actual1 = stringCalculator.Add("");
var actual2 = stringCalculator.Add(",");
// Assert
Assert.Equal(0, actual1);
Assert.Equal(0, actual2);
}
Aplicar a prática recomendada
[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add(input);
// Assert
Assert.Equal(expected, actual);
}
Validar métodos privados com métodos públicos
Na maioria dos casos, você não precisa testar um método privado em seu código. Métodos privados são um detalhe de implementação e nunca existem isoladamente. Em algum momento do processo de desenvolvimento, você apresenta um método voltado para o público para chamar o método privado como parte de sua respectiva implementação. Quando você escrever seus testes de unidade, o que deve lhe importar é o resultado final do método público que chama o privado.
Considere o seguinte cenário de código:
public string ParseLogLine(string input)
{
var sanitizedInput = TrimInput(input);
return sanitizedInput;
}
private string TrimInput(string input)
{
return input.Trim();
}
Em termos de teste, sua primeira reação pode ser escrever um teste para o TrimInput
método para garantir que ele funcione conforme o esperado. No entanto, é possível que o ParseLogLine
método manipule o sanitizedInput
objeto de uma maneira que você não espera. O comportamento desconhecido pode tornar seu teste inútil em relação ao TrimInput
método.
Um teste melhor nesse cenário é verificar o método público ParseLogLine
.
public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
var parser = new Parser();
var result = parser.ParseLogLine(" a ");
Assert.Equals("a", result);
}
Ao encontrar um método privado, localize o método público que chama o método privado e escreva seus testes para o método público. Só porque um método privado retorna um resultado esperado, não significa que o sistema que eventualmente chama o método privado usa o resultado corretamente.
Lidar com as referências estáticas de stub com junções
Um princípio de um teste de unidade é que ele deve ter controle total do sistema em teste. No entanto, esse princípio pode ser problemático quando o código de produção inclui chamadas para referências estáticas (por exemplo, DateTime.Now
).
Examine o seguinte cenário de código:
public int GetDiscountedPrice(int price)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Você pode escrever um teste de unidade para esse código? Você pode tentar realizar uma tarefa Assert no price
:
public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(2, actual)
}
public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(1, actual);
}
Infelizmente, você percebe rapidamente que há alguns problemas com seu teste:
- Se o conjunto de testes for executado na terça-feira, o segundo teste será aprovado, mas o primeiro teste falhará.
- Se o conjunto de testes for executado em qualquer outro dia, o primeiro teste será aprovado, mas o segundo teste falhará.
Para resolver esses problemas, você precisará introduzir uma junção em seu código de produção. Uma abordagem é encapsular o código que você precisa controlar em uma interface e fazer com que o código de produção dependa dessa interface:
public interface IDateTimeProvider
{
DayOfWeek DayOfWeek();
}
public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Você também precisa escrever uma nova versão do seu conjunto de testes:
public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(2, actual);
}
public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(1, actual);
}
Agora o conjunto de testes tem o controle total sobre o valor DateTime.Now
e pode executar o stub em qualquer valor quando for chamado no método.