Criando testes com variáveis anônimas através do AutoFixture

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.

  1. Install and manage packages in Visual Studio using the NuGet Package Manager | Microsoft Learn ↩︎
  2. Extension Methods – C# Programming Guide – C# | Microsoft Learn ↩︎
  3. c# – AutoFixture for number ranges – Stack Overflow ↩︎
  4. Doubly Linked list – Wikipedia ↩︎
  5. Lambda expressions – Lambda expressions and anonymous functions – C# | Microsoft Learn ↩︎

Posted

in

Luis Felipe Avatar

by

Comments

Leave a Reply

Discover more from ZBRA - Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading