Testes unitários são a barreira inicial para verificar se uma unidade funciona da maneira em que ela foi projetada dentro de um sistema de software. Muitas vezes ao testar tal componente, a primeira ideia é criar dados predefinidos e usá-los dentro das funções de teste. No entanto, essa prática pode induzir o programador a pensar que o teste – se passar – está cobrindo os casos mais triviais para o domínio do problema. Tendo em vista que o fluxo de dados em um sistema real não é estático, usar eles nos testes como input para as funções podem representar alguns problemas como: falta de confiabilidade no sistema para realizar a operação esperada e possíveis bugs não encontrados a partir do teste.
Nesse sentido, algumas libraries surgem para enfrentar o problema de dados estáticos. Uma delas, usada dentro do ambiente .NET é o AutoFixture. Ela é uma library open source com foco no desenvolvimento de testes unitários pela metodologia TDD (Test Driven Development). Seu mantra principal é que nós desenvolvedores devemos criar testes não baseados no código que implementa o algoritmo, e sim na assinatura do algoritmo. Ou seja, funções de teste não devem receber dados estáticos e sim dinâmicos, com o intuito de funcionar em qualquer caso – e é exatamente isso que o AutoFixture faz. Ela provê uma série de operações e mecanismos que podemos usar em nossos testes para criar dados dinâmicos.
Nesse post falaremos como utilizar o AutoFixture para criar estruturas autogeradas.
Preparando o ambiente
Para rodar os códigos deste post, é preciso que tenhamos o AutoFixture e outras dependências de teste instaladas1 em um projeto do Visual Studio.
Desvendando a classe a ser testada
Nesse post iremos testar classes já existentes. Isso traz um ponto positivo, pois refatoração de código existente é um dos trabalhos do desenvolvedor. Nesse sentido, iremos testar a classe Dolar – mencionada em outro post aqui no blog – que contém a seguinte implementação:
public class Dolar
{
public int Amount { get; private set; }
public Dolar(int amount)
{
Amount = amount;
}
public void Times(int multiplier)
{
Amount *= multiplier;
}
}
O método que queremos testar é o Times. Um dos testes que podemos fazer é passarmos um inteiro positivo que multiplicará a propriedade Amount. Desse modo, verificaremos se operação é feita de modo correto e se o valor Amount que um objeto da classe Dolar contém está de acordo com essa operação.
Criando testes
Criamos a classe DolarTests com o seguinte teste:
public class DolarTests
{
private readonly IFixture fixture = new Fixture();
[Fact]
public void Times_WithPositiveMultiplier_ShouldMultiplyCorrectly()
{
var initialValue = fixture.Create<int>(); //1
var dolar = new Dolar(initialValue);
var multiplier = fixture.CreateInt(minValue: 1); //2
dolar.Times(multiplier);
var result = dolar.Amount;
var expectedResult = initialValue * multiplier;
result.Should().Be(expectedResult);
}
}
A chamada padrão para se criar um objeto com variáveis autogeradas com AutoFixture é fixture.Create<tipo do objeto>(). Nesse teste, utilizamos esse método em (1) para gerar um número inteiro qualquer. Além disso, em (2) utilizamos um extension method 2 para criar um número inteiro positivo3. No final do teste comparamos o resultado do método testado com a multiplicação que foi feita manualmente.
Rodando o teste podemos verificar que ele passou sem problemas.
Ok, isso foi fácil, mas podemos simplificar o teste. Em vez de setarmos na mão o Amount de um objeto do tipo Dolar podemos deixar o fixture criá-lo. Para atingir esse resultado fazemos o fixture criar esse objeto e pegamos o Amount do objeto criado.
var dolar = fixture.Create<Dolar>(); var initialValue = dolar.Amount;
Agora a instanciação e criação de todas as propriedades e variáveis de instância de cunho público da classe Dolar fica a cargo do AutoFixture. Com ele, temos um grande poder em nossas mãos: classes podem ser criadas de maneira automática sem nos preocuparmos com sua inicialização.
Classes com referência circular
Classes com referência circular são classes que contém um tipo de relação: A tem B, B tem A. Existem alguns cenários que implementações dessa relação são inevitáveis. Uma delas é a seguinte classe que simula uma Doubly Linked List4:
public class Node
{
public Node? Next { get; set; }
public Node? Previous { get; set; }
...
}
Nessa classe, podemos perceber de cara que há uma referência circular – Node tem uma propriedade Next que é do tipo Node. Em teoria, fixture ficaria criando classes infinitamente até um erro de falta de memória surgir.
Criamos o seguinte método de teste para verificarmos a nossa teoria.
[Fact]
public void GetLastNode_WithManyNodes_ShouldReturnLast()
{
var nodes = fixture.CreateMany<Node>();
}
Esse teste utiliza o método CreateMany<Node>, que retorna um IEnumerable do tipo Node, ou seja, vários objetos do tipo Node.
Ao tentar rodar o teste, recebemos o seguinte erro:
AutoFixture.ObjectCreationExceptionWithPath : AutoFixture was unable to create an instance of type AutoFixture.Kernel.SeededRequest because the traversed object graph contains a circular reference. [xUnit.net 00:00:00.22] Request path: [xUnit.net 00:00:00.22] Peteca.Node Next [xUnit.net 00:00:00.22] Peteca.Node
Verificamos que a library contém mecanismos para que objetos não sejam criados infinitamente. No entanto, ainda estamos com o problema de não conseguirmos criar a classe com AutoFixture.
Utilizando Build para configuração de objetos
O método Build nos permite customizar a criação de um objeto. Ele nos retorna um ICustomizationComposer, que contém alguns métodos para personalizar o objeto que estamos criando. No nosso caso utilizaremos o método Without que retira da criação automática a propriedade que indicamos pela lambda expression5. O utilizamos da seguinte maneira:
var nodes = fixture
.Build<Node>()
.Without(n => n.Next)
.Without(n => n.Previous)
.CreateMany();
Nesse trecho, utilizamos o Without para retirar as propriedades Next e Previous da criação a partir do fixture.
Tendo visto o uso do Build, criamos o seguinte teste
[Fact]
public void GetLastNode_WithManyNodes_ShouldReturnLast()
{
var nodes = fixture
.Build<Node>()
.Without(n => n.Next)
.Without(n => n.Previous)
.CreateMany()
.ToList();
var initialNode = fixture
.Build<Node>()
.Without(n => n.Next)
.Without(n => n.Previous)
.Create();
var expectedLastNode = nodes.Last();
initialNode.AppendNodes(nodes);
var lastNode = initialNode.GetLastNode();
lastNode.Should().Be(expectedLastNode);
}
Nesse teste utilizamos o Build para criar vários objetos do tipo Node e adicionamos esses objetos em um node inicial por meio do AppendNodes. No final, testamos se o método GetLastNode realmente está retornando o node final.
Conclusão
Nesse post, vimos como utilizar a library AutoFixture para criar objetos individuais e vários do mesmo tipo. Conseguimos também customizar a criação desses objetos a fim de retirar referências circulares ou até mesmo propriedades que não façam sentido para o teste. Com essas ferramentas, testes podem ser feitos de maneira mais segura ao permitir que uma maior cobertura de cenários possa ser testada.
- Install and manage packages in Visual Studio using the NuGet Package Manager | Microsoft Learn ↩︎
- Extension Methods – C# Programming Guide – C# | Microsoft Learn ↩︎
- c# – AutoFixture for number ranges – Stack Overflow ↩︎
- Doubly Linked list – Wikipedia ↩︎
- Lambda expressions – Lambda expressions and anonymous functions – C# | Microsoft Learn ↩︎


Leave a Reply