Autofixture: Proposta para GenerationDepthBehavior.

Criado em 1 mai. 2018  ·  12Comentários  ·  Fonte: AutoFixture/AutoFixture

Aqui está uma proposta de comportamento do limitador de profundidade de geração.

fixture.Behaviors.Add (new GenerationDepthBehavior (2));

Tive que reimplementar ComposeIfMultiple para teste porque é interno.

`` `c #
public class GenerationDepthBehavior: ISpecimenBuilderTransformation
{
private const int DefaultGenerationDepth = 1;
private readonly int generationDepth;

public GenerationDepthBehavior() : this(DefaultGenerationDepth)
{
}

public GenerationDepthBehavior(int generationDepth)
{
    if (generationDepth < 1)
        throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");

    this.generationDepth = generationDepth;
}


public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
{
    if (builder == null) throw new ArgumentNullException(nameof(builder));

    return new GenerationDepthGuard(builder, new GenerationDepthHandler(), this.generationDepth);
}

}

interface pública IGenerationDepthHandler
{

object HandleGenerationDepthLimitRequest(object request, IEnumerable<object> recordedRequests, int depth);

}

public class DepthSeededRequest: SeededRequest
{
public int Depth {get; }
public DepthSeededRequest (solicitação de objeto, semente de objeto, profundidade interna): base (solicitação, semente)
{
Profundidade = profundidade;
}
}

public class GenerationDepthGuard: ISpecimenBuilderNode
{
ThreadLocal somente leitura privado> requestByThread
= novo ThreadLocal> (() => nova pilha());

private Stack<DepthSeededRequest> GetMonitoredRequestsForCurrentThread() => this.requestsByThread.Value;


public GenerationDepthGuard(ISpecimenBuilder builder)
    : this(builder, EqualityComparer<object>.Default)
{
}

public GenerationDepthGuard(
    ISpecimenBuilder builder,
    IGenerationDepthHandler depthHandler)
    : this(
        builder,
        depthHandler,
        EqualityComparer<object>.Default,
        1)
{
}

public GenerationDepthGuard(
    ISpecimenBuilder builder,
    IGenerationDepthHandler depthHandler,
    int generationDepth)
    : this(
        builder,
        depthHandler,
        EqualityComparer<object>.Default,
        generationDepth)
{
}


public GenerationDepthGuard(ISpecimenBuilder builder, IEqualityComparer comparer)
{
    this.Builder = builder ?? throw new ArgumentNullException(nameof(builder));
    this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
    this.GenerationDepth = 1;
}


public GenerationDepthGuard(
    ISpecimenBuilder builder,
    IGenerationDepthHandler depthHandler,
    IEqualityComparer comparer)
    : this(
    builder,
    depthHandler,
    comparer,
    1)
{
}

public GenerationDepthGuard(
    ISpecimenBuilder builder,
    IGenerationDepthHandler depthHandler,
    IEqualityComparer comparer,
    int generationDepth)
{
    if (builder == null) throw new ArgumentNullException(nameof(builder));
    if (depthHandler == null) throw new ArgumentNullException(nameof(depthHandler));
    if (comparer == null) throw new ArgumentNullException(nameof(comparer));
    if (generationDepth < 1)
        throw new ArgumentOutOfRangeException(nameof(generationDepth), "Generation depth must be greater than 0.");

    this.Builder = builder;
    this.GenerationDepthHandler = depthHandler;
    this.Comparer = comparer;
    this.GenerationDepth = generationDepth;
}


public ISpecimenBuilder Builder { get; }

public IGenerationDepthHandler GenerationDepthHandler { get; }

public int GenerationDepth { get; }

public int CurrentDepth { get; }

public IEqualityComparer Comparer { get; }

protected IEnumerable RecordedRequests => this.GetMonitoredRequestsForCurrentThread();

public virtual object HandleGenerationDepthLimitRequest(object request, int currentDepth)
{
    return this.GenerationDepthHandler.HandleGenerationDepthLimitRequest(
        request,
        this.GetMonitoredRequestsForCurrentThread(), currentDepth);
}

public object Create(object request, ISpecimenContext context)
{
    if (request is SeededRequest)
    {
        int currentDepth = -1;

        var requestsForCurrentThread = this.GetMonitoredRequestsForCurrentThread();

        if (requestsForCurrentThread.Count > 0)
        {
            currentDepth = requestsForCurrentThread.Max(x => x.Depth) + 1;
        }

        DepthSeededRequest depthRequest = new DepthSeededRequest(((SeededRequest)request).Request, ((SeededRequest)request).Seed, currentDepth);

        if (depthRequest.Depth >= this.GenerationDepth)
        {
            return HandleGenerationDepthLimitRequest(request, depthRequest.Depth);
        }

        requestsForCurrentThread.Push(depthRequest);
        try
        {
            return this.Builder.Create(request, context);
        }
        finally
        {
            requestsForCurrentThread.Pop();
        }
    }
    else
    {
        return this.Builder.Create(request, context);
    }
}

public virtual ISpecimenBuilderNode Compose(
    IEnumerable<ISpecimenBuilder> builders)
{
    var composedBuilder = ComposeIfMultiple(
        builders);
    return new GenerationDepthGuard(
        composedBuilder,
        this.GenerationDepthHandler,
        this.Comparer,
        this.GenerationDepth);
}

internal static ISpecimenBuilder ComposeIfMultiple(IEnumerable<ISpecimenBuilder> builders)
{
    ISpecimenBuilder singleItem = null;
    List<ISpecimenBuilder> multipleItems = null;
    bool hasItems = false;

    using (var enumerator = builders.GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            singleItem = enumerator.Current;
            hasItems = true;

            while (enumerator.MoveNext())
            {
                if (multipleItems == null)
                {
                    multipleItems = new List<ISpecimenBuilder> { singleItem };
                }

                multipleItems.Add(enumerator.Current);
            }
        }
    }

    if (!hasItems)
    {
        return new CompositeSpecimenBuilder();
    }

    if (multipleItems == null)
    {
        return singleItem;
    }

    return new CompositeSpecimenBuilder(multipleItems);
}

public virtual IEnumerator<ISpecimenBuilder> GetEnumerator()
{
    yield return this.Builder;
}

IEnumerator IEnumerable.GetEnumerator()
{
    return this.GetEnumerator();
}

}

public class GenerationDepthHandler: IGenerationDepthHandler
{
public object HandleGenerationDepthLimitRequest (
pedido de objeto,
IEnumerableregistradoRequests, profundidade interna)
{
retornar novo OmitSpecimen ();
}
}
`` `

question

Comentários muito úteis

@ malylemire1 Você ficaria bem se

Lamento não podermos incluir todos os recursos desejados no núcleo 😟 É um pouco difícil manter o equilíbrio entre o recurso fornecido e a quantidade de código que mantemos aqui. Parece que não será um grande problema para você implementar esse recurso ad-hoc em sua solução, portanto, você não será bloqueado.

Obrigado novamente pela sua proposta!

Todos 12 comentários

Olá, obrigado por compartilhar a ideia! : +1:

Você poderia descrever o raciocínio por trás desse recurso? Você tem algum cenário específico em que o considere útil?

Oi, eu o uso principalmente para entidades ORM ou classes API externas onde a profundidade pode se propagar indefinidamente, mas não é recursiva.
Eu uso o AutoFixture para testes de unidade, testes de integração e testes funcionais. Teste de unidade para as principais funcionalidades do framework corporativo. Testes de integração com testes para unidades complexas de trabalho com padrão de repositório e acesso a um banco de dados de teste. Teste funcional com acesso a um aplicativo completo.

Coloquei diretamente em AutoMoqDataAttribute

`` `c #
public class AutoMoqDataAttribute: AutoDataAttribute
{
///


/// Construtor padrão.
///

/// O padrão é 0 para profundidade de geração ilimitada.
public AutoMoqDataAttribute (int generationDepth = 0)
: base (() =>
{
IFixture fixture = new Fixture (). Customize (new AutoMoqCustomization ());

        fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
                .ForEach(b => fixture.Behaviors.Remove(b));

        fixture.Behaviors.Add(new OmitOnRecursionBehavior());

        if (generationDepth > 0)
        {
            fixture.Behaviors.Add(new GenerationDepthBehavior(generationDepth));
        }

        return fixture;
    })
{
}

}
`` `

Eu também sugeriria tornar o ComposeIfMultiple público. Isso tornaria a implementação do comportamento personalizado mais fácil.

Eu entendo o que o comportamento faz, a questão principal é o propósito e os cenários.

Oi, eu o uso principalmente para entidades ORM ou classes API externas onde a profundidade pode se propagar indefinidamente, mas não é recursiva.

Então, pelo que eu entendi, o principal objetivo que você está almejando é o desempenho. Do ponto de vista funcional, tudo funciona bem e você simplesmente gostaria de economizar alguns tiques da CPU.

É um pouco difícil julgar se gostaríamos de enviar esse recurso pronto para uso ou não. Eu entendo que você pode realmente ter um modelo de tipo com aninhamento muito profundo e aí o ganho de desempenho é significativo. Mas também existem algumas desvantagens nisso:

  • ele quebra a transparência de seus testes de unidade. Agora você precisa saber o nível do objeto mais profundo acessado durante o teste. Se você alterar a implementação posteriormente, os testes podem começar a falhar devido aos valores padrão e você provavelmente precisará atualizar o nível.
  • aumenta a manutenção de teste, pois agora você precisa rastrear o fato de que o gráfico pode não ter sido inicializado inteiramente.

É absolutamente normal que em sua equipe você exija esse recurso e concorde em usá-lo. No entanto, eu diria para dar a ele um uso comum, pois complica muito as coisas.

Existem muitos cenários potenciais e o AutoFixture tenta enviar apenas os recursos mais comuns. Os bits mais avançados são cobertos pelos recursos de extensibilidade. Neste caso específico, eu diria que esse recurso não é tão comum para torná-lo parte do produto. Outro ponto é que a implementação é muito pequena (veja abaixo), então não deve ser um problema implementá-la em seu projeto de teste localmente.

Eu também sugeriria tornar o ComposeIfMultiple público. Isso tornaria a implementação do comportamento personalizado mais fácil.

Já foi discutido # 657.


A propósito, a implementação pode ser um pouco simplificada (se não estiver faltando alguma coisa):
`` `c #
public class GenerationDepthBehavior: ISpecimenBuilderTransformation
{
public int Depth {get; }

public GenerationDepthBehavior(int depth)
{
    Depth = depth;
}

public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
{
    return new RecursionGuard(builder, new OmitOnRecursionHandler(), new IsSeededRequestComparer(), Depth);
}

private class IsSeededRequestComparer : IEqualityComparer
{
    bool IEqualityComparer.Equals(object x, object y)
    {
        return x is SeededRequest && y is SeededRequest;
    }

    int IEqualityComparer.GetHashCode(object obj)
    {
        return obj is SeededRequest ? 0 : EqualityComparer<object>.Default.GetHashCode(obj);
    }
}

}
`` `


@moodmosaic Você também poderia dar sua opinião? 😉

O exemplo mostrado em https://github.com/AutoFixture/AutoFixture/issues/1032#issuecomment -385724150 é a maneira preferida de usar esse comportamento ad-hoc . Neste ponto, não acho que devemos mudar o algoritmo embutido.


O que podemos considerar, no entanto, é

  • use um PRNG divisível (o atual não é divisível)
  • adicione testes de aleatoriedade (siga https://github.com/hedgehogqa/haskell-hedgehog/issues/125, por exemplo)
  • benchmarks

e ,

@moodmosaic Parece que a sugestão não é mudar o comportamento padrão, mas tornar GenerationDepthBehavior uma parte da biblioteca AutoFixture (para que você possa habilitá-la sob demanda). Então, qual a sua opinião sobre isso? :piscadela:

@zvirja Na verdade, não é por motivos de desempenho. O problema com a propagação indefinida é o teste com falha com exceção de falta de memória.

@zvirja , @ malylemire1 , a menos que esse comportamento ad-hoc trate de uma ampla gama de cenários, não acho que precise ser integrado.


Não consigo identificar um MCVE , mas AFAICT, os problemas vêm de testes automatizados que não são realmente testes de unidade (por exemplo, testes de limite / integração).

AutoFixture não foi projetada principalmente para lidar com testes de integração, então alguns comportamentos ad-hoc (como o mostrado aqui) são garantidos: +1: eu não acho que tenha que fazer parte da biblioteca principal, no entanto.

@ malylemire1 Você ficaria bem se

Lamento não podermos incluir todos os recursos desejados no núcleo 😟 É um pouco difícil manter o equilíbrio entre o recurso fornecido e a quantidade de código que mantemos aqui. Parece que não será um grande problema para você implementar esse recurso ad-hoc em sua solução, portanto, você não será bloqueado.

Obrigado novamente pela sua proposta!

Fechar este problema como parte da limpeza. Obrigado novamente por compartilhar a ideia! 😉

@ malylemire1 Compreendo 100% o caso de uso.
Isso é algo que você pode disponibilizar em um pacote nuget?

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

Questões relacionadas

gtbuchanan picture gtbuchanan  ·  3Comentários

tiesmaster picture tiesmaster  ·  7Comentários

DeafLight picture DeafLight  ·  5Comentários

ecampidoglio picture ecampidoglio  ·  7Comentários

josh-degraw picture josh-degraw  ·  4Comentários