Autofixture: Gere um objeto com restrições complexas

Criado em 26 ago. 2018  ·  8Comentários  ·  Fonte: AutoFixture/AutoFixture

Estou tentando descobrir a melhor maneira de fazer a AutoFixtura gerar um objeto que teria restrições não triviais no construtor. Por exemplo, digamos que eu queira usar uma estrutura de dados PrimeNumber que receba um int e aceite apenas números primos.

Qual seria a melhor abordagem para gerar uma instância desse tipo de estrutura no AutoFixture? Quero dizer, obviamente vou escrever uma customização, mas o que você colocaria lá?

  • Você geraria ints aleatórios e faria um loop até que um deles fosse um primo (ou executaria um algoritmo gerador de primos, é claro)? Isso poderia ser aceitável para esse tipo de restrição, mas se a restrição fosse mais difícil de cumprir, isso rapidamente se tornaria caro.
  • Você forneceria uma lista finita de alguns valores aceitáveis?

Além disso, digamos agora que estou tentando criar uma instância de algo que leva vários argumentos que podem ser teoricamente aleatórios individualmente, mas que fará alguma validação entre eles (por exemplo, argA pode estar neste intervalo de valores apenas se argB é verdadeiro e argC deve obedecer a regras de validação diferentes dependendo do valor argA, ou a propriedade argC.X deve corresponder à propriedade argA.X, algo assim).

O que você faria neste caso?

  • Uma customização para criar uma instância válida de cada tipo (sem se preocupar com nenhuma validação externa), e outra que tentaria criar o grande objeto complexo, fazendo um loop até que uma instância válida seja criada?
  • Novamente, forneça uma lista de valores finitos aceitáveis, que podem ser uma grande limitação da amplitude das possibilidades
  • Fornece uma personalização especial que criaria apenas instâncias de argumentos que se encaixariam na validação do objeto complexo

E finalmente (eu poderia ter criado vários problemas, mas senti que todos esses assuntos são aspectos diferentes do mesmo problema), tendo que criar e aplicar esse tipo de personalização cada vez que adicionamos uma nova classe, e tendo que manter essas personalizações sempre a alteração das regras de validação parece muito trabalhosa. Você aplica algumas técnicas para atenuar isso?

Muito obrigado, desculpe pela demora e espero que não seja um post muito confuso.

question

Comentários muito úteis

Dia bom! Finalmente, aloquei um pouco do tipo para responder - desculpe pela resposta extremamente tardia 😊

Em primeiro lugar, preste atenção que o núcleo do AutoFixture é bastante simples e não temos suporte embutido para árvores complexas com restrições. Resumindo, a estratégia de criação é a seguinte:

  • Procure um construtor público ou um método de fábrica estático (método estático que retorna uma instância do tipo atual).
  • Resolva os argumentos do construtor e ative a instância.
  • Preencha as propriedades públicas graváveis ​​e os campos com os valores gerados.

Com a abordagem atual, como você observou anteriormente, você não pode de alguma forma controlar as restrições de dependência.

Temos alguns pontos de personalização para especificar como construir os tipos particulares, mas eles são relativamente simples e não oferecem suporte a essas regras complexas.

Qual seria a melhor abordagem para gerar uma instância desse tipo de estrutura no AutoFixture? Quero dizer, obviamente vou escrever uma customização, mas o que você colocaria lá?

  • Você geraria ints aleatórios e faria um loop até que um deles fosse um primo (ou executaria um algoritmo gerador de primos, é claro)? Isso poderia ser aceitável para esse tipo de restrição, mas se a restrição fosse mais difícil de cumprir, isso rapidamente se tornaria caro.

  • Você forneceria uma lista finita de alguns valores aceitáveis?

Bem, infelizmente não vejo uma solução mágica aqui e a abordagem depende da situação. Se você não confiar que o valor seja muito aleatório, ou se o SUT único consumir apenas 1-2 números primos, então pode ser bom codificar os números primos e selecioná-los (temos ElementsBulider<> auxiliar embutido para esses casos). Por outro lado, se você precisa de uma grande lista de números primos e opera com longas sequências de números primos, provavelmente é melhor codificar um algoritmo para gerá-los dinamicamente.

Além disso, digamos agora que estou tentando criar uma instância de algo que leva vários argumentos que podem ser teoricamente aleatórios individualmente, mas que fará alguma validação entre eles (por exemplo, argA pode estar neste intervalo de valores apenas se argB é verdadeiro e argC deve obedecer a regras de validação diferentes dependendo do valor argA, ou a propriedade argC.X deve corresponder à propriedade argA.X, algo assim).

O que você faria neste caso?

Realmente uma boa pergunta e, infelizmente, o AutoFixture não permite resolvê-la de uma maneira agradável e pronta para uso. Normalmente, estou tentando isolar as personalizações para cada tipo, portanto, a personalização para um tipo controla a criação de um único tipo apenas. Mas, nos meus casos, os tipos são independentes e, obviamente, não funcionará bem no seu caso. Além disso, o AutoFixture não fornece contexto pronto para uso, portanto, quando você está escrevendo uma personalização para um tipo específico, não pode compreender claramente o contexto no qual está criando um objeto (chamado internamente de espécime).

No topo da minha cabeça, eu diria que normalmente recomendaria a seguinte estratégia:

  • Tente criar personalização para cada tipo de forma que controle a criação de um único tipo de objeto apenas.
  • Se você precisar criar dependências com restrições específicas, é melhor ativar essas dependências na customização também. Se sua dependência for mutável, você pode pedir ao AutoFixture para criar a dependência para você e depois configurá-la de uma forma que se torne compatível.

Desta forma, você não contradiria muito a arquitetura interna e ficará claro como ela funciona. É claro que, potencialmente, essa forma é muito prolixa.

Se os casos com restrições complexas não forem tão comuns, os recursos existentes podem ser suficientes para você. Mas se o seu modelo de domínio estiver realmente cheio de tais casos, francamente, a AutoFixture pode não ser a melhor ferramenta para você. Provavelmente, existem ferramentas melhores no mercado que permitem resolver tais problemas da forma mais elegante. Claro, vale a pena mencionar que o AutoFixture é muito flexível e você pode sobrescrever quase tudo, então você sempre pode criar sua própria DSL em cima do núcleo do AutoFixture ... Mas você deve avaliar qual caminho é mais barato para você 😉

Vamos também perguntar a @ploeh o que pensa. Normalmente, as respostas de Mark são profundas e ele tenta encontrar a causa raiz primeiro, em vez de resolver as consequências 😅

Se você tiver mais perguntas - pergunte! Serei sempre bem-vindo para respondê-las.

PS FWIW, decidi fornecer a vocês um exemplo, onde tentei brincar com o AutoFixture e resolver um problema semelhante (tentei mantê-lo simples e pode não funcionar totalmente no seu caso):


Clique para ver o código fonte

`` `c #
using System;
usando AutoFixture;
using AutoFixture.Xunit2;
using Xunit;

namespace AutoFixturePlayground
{
public static class Util
{
public static bool IsPrime (número int)
{
// Copiado de https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
`` `

Todos 8 comentários

Desculpe pelo silêncio do rádio. Estamos vivos e responderei em breve - estou extremamente ocupado com meu trabalho principal atualmente. Também estou trabalhando no lançamento do NSubstitute v4, então o tempo é um recurso muito limitado: pensativo: a pergunta é difícil, então pense em todas as maneiras possíveis antes de postar a resposta.

Obrigado pela paciência, continue sintonizando: wink:

Oi,
Alguma notícia sobre isso?
Sem pressão (eu conheço o procedimento 😄, além disso, não está bloqueando realmente, eu realmente gostaria de alguns conselhos bem informados), é apenas para saber se você tem alguma visibilidade.
Muito obrigado!

Dia bom! Finalmente, aloquei um pouco do tipo para responder - desculpe pela resposta extremamente tardia 😊

Em primeiro lugar, preste atenção que o núcleo do AutoFixture é bastante simples e não temos suporte embutido para árvores complexas com restrições. Resumindo, a estratégia de criação é a seguinte:

  • Procure um construtor público ou um método de fábrica estático (método estático que retorna uma instância do tipo atual).
  • Resolva os argumentos do construtor e ative a instância.
  • Preencha as propriedades públicas graváveis ​​e os campos com os valores gerados.

Com a abordagem atual, como você observou anteriormente, você não pode de alguma forma controlar as restrições de dependência.

Temos alguns pontos de personalização para especificar como construir os tipos particulares, mas eles são relativamente simples e não oferecem suporte a essas regras complexas.

Qual seria a melhor abordagem para gerar uma instância desse tipo de estrutura no AutoFixture? Quero dizer, obviamente vou escrever uma customização, mas o que você colocaria lá?

  • Você geraria ints aleatórios e faria um loop até que um deles fosse um primo (ou executaria um algoritmo gerador de primos, é claro)? Isso poderia ser aceitável para esse tipo de restrição, mas se a restrição fosse mais difícil de cumprir, isso rapidamente se tornaria caro.

  • Você forneceria uma lista finita de alguns valores aceitáveis?

Bem, infelizmente não vejo uma solução mágica aqui e a abordagem depende da situação. Se você não confiar que o valor seja muito aleatório, ou se o SUT único consumir apenas 1-2 números primos, então pode ser bom codificar os números primos e selecioná-los (temos ElementsBulider<> auxiliar embutido para esses casos). Por outro lado, se você precisa de uma grande lista de números primos e opera com longas sequências de números primos, provavelmente é melhor codificar um algoritmo para gerá-los dinamicamente.

Além disso, digamos agora que estou tentando criar uma instância de algo que leva vários argumentos que podem ser teoricamente aleatórios individualmente, mas que fará alguma validação entre eles (por exemplo, argA pode estar neste intervalo de valores apenas se argB é verdadeiro e argC deve obedecer a regras de validação diferentes dependendo do valor argA, ou a propriedade argC.X deve corresponder à propriedade argA.X, algo assim).

O que você faria neste caso?

Realmente uma boa pergunta e, infelizmente, o AutoFixture não permite resolvê-la de uma maneira agradável e pronta para uso. Normalmente, estou tentando isolar as personalizações para cada tipo, portanto, a personalização para um tipo controla a criação de um único tipo apenas. Mas, nos meus casos, os tipos são independentes e, obviamente, não funcionará bem no seu caso. Além disso, o AutoFixture não fornece contexto pronto para uso, portanto, quando você está escrevendo uma personalização para um tipo específico, não pode compreender claramente o contexto no qual está criando um objeto (chamado internamente de espécime).

No topo da minha cabeça, eu diria que normalmente recomendaria a seguinte estratégia:

  • Tente criar personalização para cada tipo de forma que controle a criação de um único tipo de objeto apenas.
  • Se você precisar criar dependências com restrições específicas, é melhor ativar essas dependências na customização também. Se sua dependência for mutável, você pode pedir ao AutoFixture para criar a dependência para você e depois configurá-la de uma forma que se torne compatível.

Desta forma, você não contradiria muito a arquitetura interna e ficará claro como ela funciona. É claro que, potencialmente, essa forma é muito prolixa.

Se os casos com restrições complexas não forem tão comuns, os recursos existentes podem ser suficientes para você. Mas se o seu modelo de domínio estiver realmente cheio de tais casos, francamente, a AutoFixture pode não ser a melhor ferramenta para você. Provavelmente, existem ferramentas melhores no mercado que permitem resolver tais problemas da forma mais elegante. Claro, vale a pena mencionar que o AutoFixture é muito flexível e você pode sobrescrever quase tudo, então você sempre pode criar sua própria DSL em cima do núcleo do AutoFixture ... Mas você deve avaliar qual caminho é mais barato para você 😉

Vamos também perguntar a @ploeh o que pensa. Normalmente, as respostas de Mark são profundas e ele tenta encontrar a causa raiz primeiro, em vez de resolver as consequências 😅

Se você tiver mais perguntas - pergunte! Serei sempre bem-vindo para respondê-las.

PS FWIW, decidi fornecer a vocês um exemplo, onde tentei brincar com o AutoFixture e resolver um problema semelhante (tentei mantê-lo simples e pode não funcionar totalmente no seu caso):


Clique para ver o código fonte

`` `c #
using System;
usando AutoFixture;
using AutoFixture.Xunit2;
using Xunit;

namespace AutoFixturePlayground
{
public static class Util
{
public static bool IsPrime (número int)
{
// Copiado de https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
`` `

Oi @zvirja

Nossa, obrigado pela resposta detalhada, é muito interessante. Vou ter que fazer alguns testes e estimar o que vale a pena fazer ou não, mas no geral isso é ótimo.

Não acho que tenha tantas dependências para lidar, então sua abordagem pode ser uma boa opção. Claro, se @ploeh tiver algo mais a acrescentar, eu ficaria honrado 👌

Obrigado novamente, continue com o bom trabalho!

Minha experiência com o AutoFixture e o teste baseado em propriedade é que existem basicamente duas maneiras de resolver problemas como estes:

  • Filtrando
  • Criação Algorítmica

(Enquanto escrevo, minha intuição sugere que podem ser _catamorfismos_ e _anamorfismos_, respectivamente, mas terei que pensar um pouco mais sobre isso, portanto, isso à parte é principalmente uma observação para mim mesmo.)

Se _mais_ valores gerados aleatoriamente se ajustassem a quaisquer restrições dentro das quais se deva caber, então usar um gerador existente, mas jogar fora o valor inadequado ocasional, poderia ser a maneira mais fácil de resolver o problema.

Se, por outro lado, um filtro significaria jogar fora a maioria dos dados aleatórios, você teria que criar um algoritmo que, talvez baseado em valores iniciais aleatórios, geraria valores que se ajustassem às restrições em questão.

Há alguns anos, dei uma palestra mostrando alguns exemplos simples de ambas as abordagens no contexto do FsCheck . Esta apresentação é na verdade uma evolução de uma palestra que teve a mesma abordagem, mas apenas com o AutoFixture. Infelizmente, não existe nenhuma gravação dessa palestra.

Pode-se atender ao requisito dos números primos de ambas as maneiras.

A abordagem do filtro seria gerar números irrestritos e, em seguida, jogar os números fora até obter um que seja, de fato, primo.

A abordagem algorítmica seria usar um algoritmo como uma peneira primo para gerar um número primo. No entanto, isso não é aleatório, então você pode querer descobrir como randomizá-lo.

A questão geral de como lidar com valores restritos no AutoFixture surgiu quase imediatamente depois que outras pessoas começaram a olhar para a biblioteca, e eu escrevi um artigo que ainda menciono: http://blog.ploeh.dk/2009/ 05/01 / DealingWithConstrainedInput

Quanto à questão dos múltiplos valores que se relacionam entre si, não desejo dar nenhuma orientação geral. Esses tipos de perguntas costumam ser problemas XY. Em muitos casos, uma vez que entendo os detalhes, um design alternativo pode resolver problemas não apenas com o AutoFixture, mas também com a própria base de código de produção.

Mesmo na presença de problemas XY, no entanto, ainda haverá situações em que isso pode ser uma preocupação legítima, mas prefiro lidar com elas caso a caso, pois, em minha experiência, elas são cru.

Portanto, se você tiver um exemplo específico disso, posso ajudar, mas acho que não posso responder de forma significativa à pergunta geral.

@ploeh Muito obrigado por esta resposta, que confirma as abordagens que eu estava considerando (e você me deixou curioso sobre catálogos e anamorfismos 😃).
Eu concordo totalmente que os valores interdependentes são principalmente um problema XY (pelo menos no meu caso), a questão é que ao trabalhar em código legado (não testado 😢), lidar com esses valores foi um bom começo para escrever alguns testes de qualquer maneira, até chegarmos o tempo para refatorar isso corretamente.

De qualquer forma, ambas as suas respostas abordam o problema muito bem, acho que estou pronto para continuar a partir daí.
Obrigado!

Aliás, esqueci de mencionar que minha resposta apenas foi considerada um acréscimo à de @zvirja . Já é uma boa resposta aí 👍

Eu não entendi de outra forma 😄

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

Ridermansb picture Ridermansb  ·  4Comentários

Accc99 picture Accc99  ·  4Comentários

tomasaschan picture tomasaschan  ·  3Comentários

ploeh picture ploeh  ·  3Comentários

gtbuchanan picture gtbuchanan  ·  3Comentários