Autofixture: Proposition de GenerationDepthBehavior.

Créé le 1 mai 2018  ·  12Commentaires  ·  Source: AutoFixture/AutoFixture

Voici une proposition de comportement du limiteur de profondeur de génération.

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

J'ai dû réimplémenter ComposeIfMultiple pour le test car il est interne.

```c#
classe publique 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 publique IGenerationDepthHandler
{

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

}

classe publique DepthSeededRequest : SeededRequest
{
public int Profondeur { get; }
public DepthSeededRequest(requête d'objet, graine d'objet, profondeur int) : base(requête, graine)
{
Profondeur = profondeur ;
}
}

classe publique GenerationDepthGuard : ISpecimenBuilderNode
{
ThreadLocal privé en lecture seule> demandesParThread
= nouveau ThreadLocal>(() => nouvelle pile());

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();
}

}

classe publique GenerationDepthHandler : IGenerationDepthHandler
{
objet public HandleGenerationDepthLimitRequest(
demande d'objet,
IEnumerableRequêtes enregistrées, profondeur)
{
retourner un nouveau OmitSpecimen();
}
}
```

question

Commentaire le plus utile

@malylemire1 Seriez-vous d' reportions cela pour l'instant et ne l'implémentions plus tard que si nous voyons beaucoup de demandes pour cette fonctionnalité ?

Je suis désolé que nous ne puissions pas inclure toutes les fonctionnalités souhaitées dans le noyau 😟 Il est un peu difficile de maintenir l'équilibre entre la fonctionnalité fournie et la quantité de code que nous maintenons ici. Il semble que ce ne sera pas un gros problème pour vous d'implémenter cette fonctionnalité ad-hoc dans votre solution, vous n'êtes donc pas bloqué.

Merci encore pour votre proposition !

Tous les 12 commentaires

Bonjour, merci pour le partage de l'idée ! :+1:

Pourriez-vous s'il vous plaît décrire le raisonnement derrière cette fonctionnalité? Avez-vous un scénario particulier où vous l'avez trouvé utile?

Bonjour, je l'utilise principalement pour les entités ORM ou les classes d'API externes où la profondeur peut se propager indéfiniment mais n'est pas récursive.
J'utilise AutoFixture pour les tests unitaires, les tests d'intégration et les tests fonctionnels. Test unitaire pour les fonctionnalités de base du framework d'entreprise. Tests d'intégration avec tests d'unités de travail complexes avec modèle de référentiel et accès à une base de données de test. Test fonctionnel avec accès à une application complète.

Je l'ai mis directement sur AutoMoqDataAttribute

```c#
classe publique AutoMoqDataAttribute : AutoDataAttribute
{
///


/// Constructeur par défaut.
///

/// La valeur par défaut est 0 pour une profondeur de génération illimitée.
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;
    })
{
}

}
```

Je suggérerais également de rendre ComposeIfMultiple public. Cela faciliterait la mise en œuvre du comportement personnalisé.

Je comprends ce que fait le comportement, la question principale est le but et les scénarios.

Bonjour, je l'utilise principalement pour les entités ORM ou les classes d'API externes où la profondeur peut se propager indéfiniment mais n'est pas récursive.

Donc, pour autant que je sache, l'objectif principal que vous visez est la performance. Du point de vue fonctionnel, tout fonctionne bien et vous souhaitez simplement économiser quelques ticks CPU.

Il est un peu difficile de juger si nous aimerions livrer avec cette fonctionnalité hors de la boîte ou non. Je comprends que vous pourriez en effet avoir un modèle de type avec une imbrication très profonde et là, le gain de performance est important. Mais il y a aussi quelques inconvénients à cela :

  • il brise la transparence de vos tests unitaires. Vous devez maintenant connaître le niveau de l'objet le plus accessible pendant le test. Si vous modifiez ultérieurement l'implémentation, les tests peuvent commencer à échouer en raison des valeurs par défaut et vous devrez probablement mettre à jour le niveau.
  • cela augmente la maintenance des tests, car vous devez maintenant suivre le fait que le graphique n'est peut-être pas entièrement initialisé.

C'est tout à fait normal que dans votre équipe, vous ayez besoin de cette fonctionnalité et que vous acceptiez de l'utiliser. Cependant, je dirais de lui donner un usage courant car cela complique beaucoup les choses.

Il existe de nombreux scénarios potentiels et AutoFixture essaie de n'offrir que les fonctionnalités les plus courantes. Les bits les plus avancés sont couverts par les capacités d'extensibilité. Dans ce cas particulier, je dirais que cette fonctionnalité n'est pas si courante pour en faire une partie du produit. Un autre point est que l'implémentation est très petite (voir ci-dessous), donc cela ne devrait pas être un problème de l'implémenter localement dans votre projet de test.

Je suggérerais également de rendre ComposeIfMultiple public. Cela faciliterait la mise en œuvre du comportement personnalisé.

Il a déjà été discuté #657.


Soit dit en passant, la mise en œuvre pourrait être un peu simplifiée (si je ne manque pas quelque chose):
```c#
classe publique GenerationDepthBehavior : ISpecimenBuilderTransformation
{
public int Profondeur { 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 Pourriez-vous également partager votre opinion ? ??

L'exemple montré dans https://github.com/AutoFixture/AutoFixture/issues/1032#issuecomment -385724150 est le moyen préféré d'utiliser ce comportement ad-hoc . À ce stade, je ne pense pas que nous devrions changer l'algorithme intégré.


Ce que nous pourrions envisager, cependant, est de

  • utiliser un PRNG fractionnable (l'actuel n'est pas fractionnable)
  • ajouter des tests aléatoires (suivez https://github.com/hedgehogqa/haskell-hedgehog/issues/125, par exemple)
  • repères

et ensuite , nous pourrions envisager d'effectuer d'autres optimisations, comme celle-ci.

@moodmosaic Il semble que la suggestion ne soit GenerationDepthBehavior une partie de la bibliothèque AutoFixture (afin que vous puissiez l'activer à la demande). Alors quel est votre avis à ce sujet ? :clin d'œil:

@zvirja Ce n'est pas vraiment pour des raisons de performances. Le problème avec la propagation indéfinie est l'échec du test avec une exception de mémoire insuffisante.

@zvirja , @malylemire1 , à moins que ce comportement ad hoc ne traite un large éventail de scénarios, je ne pense pas qu'il doive être intégré.


Je ne peux pas repérer un MCVE , mais AFAICT, les problèmes proviennent de tests automatisés qui ne sont pas réellement des tests unitaires (par exemple, des tests de limites/d'intégration).

AutoFixture n'a pas été principalement conçu pour traiter les tests d'intégration, donc certains comportements ad-hoc (comme celui montré ici) sont garantis :+1: Je ne pense pas que cela doive faire partie de la bibliothèque principale, cependant.

@malylemire1 Seriez-vous d' reportions cela pour l'instant et ne l'implémentions plus tard que si nous voyons beaucoup de demandes pour cette fonctionnalité ?

Je suis désolé que nous ne puissions pas inclure toutes les fonctionnalités souhaitées dans le noyau 😟 Il est un peu difficile de maintenir l'équilibre entre la fonctionnalité fournie et la quantité de code que nous maintenons ici. Il semble que ce ne sera pas un gros problème pour vous d'implémenter cette fonctionnalité ad-hoc dans votre solution, vous n'êtes donc pas bloqué.

Merci encore pour votre proposition !

Fermeture de ce problème dans le cadre du nettoyage. Merci encore d'avoir partagé l'idée ! ??

@malylemire1 Je comprends à 100% le cas d'utilisation.
Est-ce quelque chose que vous pouvez rendre disponible dans un package nuget ?

Cette page vous a été utile?
0 / 5 - 0 notes