Autofixture: Générer un objet avec des contraintes complexes

Créé le 26 août 2018  ·  8Commentaires  ·  Source: AutoFixture/AutoFixture

J'essaie de trouver le meilleur moyen de faire en sorte que AutoFixture génère un objet qui aurait des contraintes non triviales dans le constructeur. Par exemple, disons que je souhaite utiliser une structure de données PrimeNumber qui prendrait un entier et n'accepterait que les nombres premiers.

Quelle serait la meilleure approche pour générer une instance de ce type de structure dans AutoFixture ? Je veux dire, je vais évidemment écrire une personnalisation, mais que mettriez-vous là-dedans ?

  • Souhaitez-vous générer des entiers aléatoires et boucler jusqu'à ce que l'un d'eux soit un nombre premier (ou exécuter un algorithme de génération de nombres premiers, bien sûr) ? Cela pourrait être acceptable pour ce genre de contrainte, mais si la contrainte était plus délicate à respecter, cela deviendrait vite coûteux.
  • Pourriez-vous fournir une liste finie de quelques valeurs acceptables ?

De plus, disons maintenant que j'essaie de créer une instance de quelque chose qui prend plusieurs arguments qui peuvent théoriquement être aléatoires individuellement, mais qui effectuera une validation entre eux (par exemple, argA ne peut être dans cette plage de valeurs que si argB est vrai, et argC doit se conformer à différentes règles de validation en fonction de la valeur argA, ou la propriété argC.X doit correspondre à la propriété argA.X, des trucs comme ça).

Que feriez-vous dans ce cas ?

  • Une personnalisation pour créer une instance valide de chaque type (sans se soucier d'aucune validation externe), et une autre qui essaierait de créer le gros objet complexe, en boucle jusqu'à ce qu'une instance valide soit créée ?
  • Encore une fois, fournissez une liste de valeurs acceptables finies, qui peuvent être une limitation sévère de l'amplitude des possibilités
  • Fournir une personnalisation spéciale qui créerait uniquement des instances d'arguments qui correspondraient à la validation de l'objet complexe

Et enfin (j'aurais pu créer plusieurs problèmes, mais j'avais l'impression que tous ces sujets sont des aspects différents du même problème), devoir créer et appliquer ce type de personnalisations chaque fois que nous ajoutons une nouvelle classe, et devoir maintenir ces personnalisations à chaque fois le changement des règles de validation semble être beaucoup de travail, appliquez-vous des techniques pour atténuer cela ?

Merci beaucoup, désolé pour le long et j'espère que le post n'est pas trop salissant.

question

Commentaire le plus utile

Bonne journée! Enfin, j'ai alloué un peu de type pour répondre - désolé pour la réponse extrêmement tardive

Tout d'abord, faites attention au fait que le noyau d'AutoFixture est assez simple et que nous n'avons pas de support intégré pour les arbres complexes avec des contraintes. En bref, la stratégie de création est la suivante :

  • Recherchez un constructeur public ou une méthode de fabrique statique (méthode statique renvoyant une instance du type actuel).
  • Résolvez les arguments du constructeur et activez l'instance.
  • Remplissez les propriétés et les champs publics accessibles en écriture avec les valeurs générées.

Avec l'approche actuelle, comme vous l'avez remarqué précédemment, vous ne pouvez pas contrôler d'une manière ou d'une autre les contraintes de dépendance.

Nous avons quelques points de personnalisation pour spécifier comment construire les types particuliers, mais ils sont relativement simples et ne prennent pas en charge ces règles complexes.

Quelle serait la meilleure approche pour générer une instance de ce type de structure dans AutoFixture ? Je veux dire, je vais évidemment écrire une personnalisation, mais que mettriez-vous là-dedans ?

  • Souhaitez-vous générer des entiers aléatoires et boucler jusqu'à ce que l'un d'eux soit un nombre premier (ou exécuter un algorithme de génération de nombres premiers, bien sûr) ? Cela pourrait être acceptable pour ce genre de contrainte, mais si la contrainte était plus délicate à respecter, cela deviendrait vite coûteux.

  • Pourriez-vous fournir une liste finie de quelques valeurs acceptables ?

Eh bien, malheureusement, je ne vois pas de solution miracle ici et l'approche dépend de la situation. Si vous ne comptez pas sur la valeur pour être trop aléatoire, ou si un seul SUT ne consomme que 1-2 nombres premiers, alors il peut être judicieux de coder en dur les nombres premiers et de les choisir (nous avons ElementsBulider<> assistant intégré pour ces cas). D'un autre côté, si vous avez besoin d'une grande liste de nombres premiers et que vous travaillez avec de longues séquences de nombres premiers, il est probablement préférable de coder un algorithme pour les générer dynamiquement.

De plus, disons maintenant que j'essaie de créer une instance de quelque chose qui prend plusieurs arguments qui peuvent théoriquement être aléatoires individuellement, mais qui effectuera une validation entre eux (par exemple, argA ne peut être dans cette plage de valeurs que si argB est vrai, et argC doit se conformer à différentes règles de validation en fonction de la valeur argA, ou la propriété argC.X doit correspondre à la propriété argA.X, des trucs comme ça).

Que feriez-vous dans ce cas ?

Vraiment une bonne question et malheureusement, AutoFixture ne permet pas de la résoudre de manière agréable et prête à l'emploi. Habituellement, j'essaie d'isoler les personnalisations pour chaque type, donc la personnalisation pour un type contrôle la création pour un seul type uniquement. Mais dans mes cas les types sont indépendants et évidemment cela ne fonctionnera pas bien dans votre cas. De plus, AutoFixture ne fournit pas de contexte prêt à l'emploi, donc lorsque vous écrivez une personnalisation pour un type particulier, vous ne pouvez pas clairement comprendre le contexte dans lequel vous créez un objet (appelé en interne spécimen).

En plus de ma tête, je dirais que je recommanderais généralement la stratégie suivante :

  • Essayez de créer une personnalisation pour chaque type de manière à contrôler la création d'un seul type d'objet uniquement.
  • Si vous devez créer des dépendances avec des contraintes particulières, il est préférable d'activer également ces dépendances dans la personnalisation. Si votre dépendance est modifiable, vous pouvez demander à AutoFixture de créer la dépendance pour vous et de la configurer plus tard de manière à ce qu'elle devienne compatible.

De cette façon, vous ne contrediriez pas trop l'architecture interne et son fonctionnement sera clair. Bien sûr, potentiellement cette façon est très verbeuse.

Si les cas avec des contraintes complexes ne sont pas si courants, les capacités existantes peuvent vous suffire. Mais si votre modèle de domaine regorge vraiment de tels cas, franchement, AutoFixture n'est peut-être pas le meilleur outil pour vous. Il existe probablement de meilleurs outils sur le marché qui permettent de résoudre de tels problèmes de la manière la plus élégante. Bien sûr, il convient de mentionner qu'AutoFixture est très flexible et que vous pouvez remplacer presque tout, vous pouvez donc toujours créer votre propre DSL en plus du noyau AutoFixture...

Demandons également à @ploeh son avis. Habituellement, les réponses de Mark sont profondes et il essaie d'abord de trouver la cause première, plutôt que de résoudre les conséquences 😅

Si vous avez d'autres questions, n'hésitez pas ! Je serai toujours le bienvenu pour y répondre.

PS FWIW, j'ai décidé de vous fournir un exemple, où j'ai essayé de jouer avec AutoFixture et de résoudre un problème similaire (j'ai essayé de rester simple et cela pourrait ne pas fonctionner entièrement dans votre cas):


Cliquez pour voir le code source

```c#
en utilisant le système ;
en utilisant AutoFixture ;
en utilisant AutoFixture.Xunit2 ;
en utilisant Xunit ;

espace de noms AutoFixturePlayground
{
classe statique publique Util
{
bool statique public IsPrime (nombre entier)
{
// Copié depuis 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);
}

}
```

Tous les 8 commentaires

Désolé pour le silence radio. Nous sommes vivants et je vous répondrai bientôt - je suis extrêmement occupé par mon travail principal ces jours-ci. Travaille également sur la version NSubstitute v4, donc le temps est très limité.

Merci pour la patience, restez connectés :wink:

Salut,
Des nouvelles là-dessus ?
Pas de pression (je connais l'exercice 😄 , en plus ce n'est pas vraiment bloquant, j'aimerais juste vraiment des conseils avisés), c'est juste pour savoir si tu as une certaine visibilité.
Merci beaucoup!

Bonne journée! Enfin, j'ai alloué un peu de type pour répondre - désolé pour la réponse extrêmement tardive

Tout d'abord, faites attention au fait que le noyau d'AutoFixture est assez simple et que nous n'avons pas de support intégré pour les arbres complexes avec des contraintes. En bref, la stratégie de création est la suivante :

  • Recherchez un constructeur public ou une méthode de fabrique statique (méthode statique renvoyant une instance du type actuel).
  • Résolvez les arguments du constructeur et activez l'instance.
  • Remplissez les propriétés et les champs publics accessibles en écriture avec les valeurs générées.

Avec l'approche actuelle, comme vous l'avez remarqué précédemment, vous ne pouvez pas contrôler d'une manière ou d'une autre les contraintes de dépendance.

Nous avons quelques points de personnalisation pour spécifier comment construire les types particuliers, mais ils sont relativement simples et ne prennent pas en charge ces règles complexes.

Quelle serait la meilleure approche pour générer une instance de ce type de structure dans AutoFixture ? Je veux dire, je vais évidemment écrire une personnalisation, mais que mettriez-vous là-dedans ?

  • Souhaitez-vous générer des entiers aléatoires et boucler jusqu'à ce que l'un d'eux soit un nombre premier (ou exécuter un algorithme de génération de nombres premiers, bien sûr) ? Cela pourrait être acceptable pour ce genre de contrainte, mais si la contrainte était plus délicate à respecter, cela deviendrait vite coûteux.

  • Pourriez-vous fournir une liste finie de quelques valeurs acceptables ?

Eh bien, malheureusement, je ne vois pas de solution miracle ici et l'approche dépend de la situation. Si vous ne comptez pas sur la valeur pour être trop aléatoire, ou si un seul SUT ne consomme que 1-2 nombres premiers, alors il peut être judicieux de coder en dur les nombres premiers et de les choisir (nous avons ElementsBulider<> assistant intégré pour ces cas). D'un autre côté, si vous avez besoin d'une grande liste de nombres premiers et que vous travaillez avec de longues séquences de nombres premiers, il est probablement préférable de coder un algorithme pour les générer dynamiquement.

De plus, disons maintenant que j'essaie de créer une instance de quelque chose qui prend plusieurs arguments qui peuvent théoriquement être aléatoires individuellement, mais qui effectuera une validation entre eux (par exemple, argA ne peut être dans cette plage de valeurs que si argB est vrai, et argC doit se conformer à différentes règles de validation en fonction de la valeur argA, ou la propriété argC.X doit correspondre à la propriété argA.X, des trucs comme ça).

Que feriez-vous dans ce cas ?

Vraiment une bonne question et malheureusement, AutoFixture ne permet pas de la résoudre de manière agréable et prête à l'emploi. Habituellement, j'essaie d'isoler les personnalisations pour chaque type, donc la personnalisation pour un type contrôle la création pour un seul type uniquement. Mais dans mes cas les types sont indépendants et évidemment cela ne fonctionnera pas bien dans votre cas. De plus, AutoFixture ne fournit pas de contexte prêt à l'emploi, donc lorsque vous écrivez une personnalisation pour un type particulier, vous ne pouvez pas clairement comprendre le contexte dans lequel vous créez un objet (appelé en interne spécimen).

En plus de ma tête, je dirais que je recommanderais généralement la stratégie suivante :

  • Essayez de créer une personnalisation pour chaque type de manière à contrôler la création d'un seul type d'objet uniquement.
  • Si vous devez créer des dépendances avec des contraintes particulières, il est préférable d'activer également ces dépendances dans la personnalisation. Si votre dépendance est modifiable, vous pouvez demander à AutoFixture de créer la dépendance pour vous et de la configurer plus tard de manière à ce qu'elle devienne compatible.

De cette façon, vous ne contrediriez pas trop l'architecture interne et son fonctionnement sera clair. Bien sûr, potentiellement cette façon est très verbeuse.

Si les cas avec des contraintes complexes ne sont pas si courants, les capacités existantes peuvent vous suffire. Mais si votre modèle de domaine regorge vraiment de tels cas, franchement, AutoFixture n'est peut-être pas le meilleur outil pour vous. Il existe probablement de meilleurs outils sur le marché qui permettent de résoudre de tels problèmes de la manière la plus élégante. Bien sûr, il convient de mentionner qu'AutoFixture est très flexible et que vous pouvez remplacer presque tout, vous pouvez donc toujours créer votre propre DSL en plus du noyau AutoFixture...

Demandons également à @ploeh son avis. Habituellement, les réponses de Mark sont profondes et il essaie d'abord de trouver la cause première, plutôt que de résoudre les conséquences 😅

Si vous avez d'autres questions, n'hésitez pas ! Je serai toujours le bienvenu pour y répondre.

PS FWIW, j'ai décidé de vous fournir un exemple, où j'ai essayé de jouer avec AutoFixture et de résoudre un problème similaire (j'ai essayé de rester simple et cela pourrait ne pas fonctionner entièrement dans votre cas):


Cliquez pour voir le code source

```c#
en utilisant le système ;
en utilisant AutoFixture ;
en utilisant AutoFixture.Xunit2 ;
en utilisant Xunit ;

espace de noms AutoFixturePlayground
{
classe statique publique Util
{
bool statique public IsPrime (nombre entier)
{
// Copié depuis 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);
}

}
```

Salut @zvirja

Wow, merci pour la réponse détaillée, c'est vraiment intéressant. Je vais devoir faire quelques tests et estimer ce que ça vaut la peine de faire ou pas, mais dans l'ensemble c'est super.

Je ne pense pas avoir autant de dépendances à gérer, donc votre approche pourrait être une bonne voie à suivre. Bien sûr, si @ploeh a quelque chose de plus à ajouter, j'en serais honoré 👌

Merci encore, continuez votre bon travail!

Mon expérience avec les tests AutoFixture et basés sur les propriétés est qu'il existe essentiellement deux façons de résoudre des problèmes comme ceux-ci :

  • Filtration
  • Création algorithmique

(Au moment où j'écris, mon intuition suggère que ceux-ci pourraient être des _catamorphismes_ et des _anamorphismes_, respectivement, mais je devrai y réfléchir un peu plus, donc cet aparté est principalement une note pour moi-même.)

Si _la plupart_ des valeurs générées de manière aléatoire correspondaient à toutes les contraintes auxquelles on doit s'adapter, alors l'utilisation d'un générateur existant, mais en jetant la valeur occasionnelle inadaptée, pourrait être le moyen le plus simple de résoudre le problème.

Si, d'un autre côté, un filtre signifiait jeter la plupart des données aléatoires, vous devriez plutôt trouver un algorithme qui, peut-être basé sur des valeurs de départ aléatoires, générera des valeurs qui correspondent aux contraintes en question.

Il y a quelques années, j'ai donné une conférence montrant quelques exemples simples des deux approches dans le contexte de FsCheck . Cette présentation est en fait une évolution d'une conférence qui a adopté la même approche, mais juste avec AutoFixture à la place. Malheureusement, il n'existe aucun enregistrement de cette conversation.

On pourrait répondre à l'exigence du nombre premier des deux manières.

L'approche par filtre consisterait à générer des nombres sans contraintes, puis à jeter les nombres jusqu'à ce que vous en obteniez un qui soit effectivement un nombre premier.

L'approche algorithmique serait d'utiliser un algorithme comme un tamis premier pour générer un nombre premier. Ce n'est pas aléatoire, cependant, alors on pourrait vouloir comprendre comment le randomiser.

La question générale de savoir comment gérer les valeurs contraintes dans AutoFixture s'est posée presque immédiatement une fois que d'autres personnes ont commencé à consulter la bibliothèque, et j'ai écrit à l'époque un article auquel je me réfère toujours : http://blog.ploeh.dk/2009/ 05/01/TraiterAvecEntrée Contrainte

Concernant la question des valeurs multiples qui se rapportent les unes aux autres, je ne souhaite donner aucune orientation générale. Ce genre de questions sont souvent des problèmes XY. Dans de nombreux cas, une fois que j'ai compris les détails, une conception alternative pourrait résoudre des problèmes non seulement avec AutoFixture, mais également avec la base de code de production elle-même.

Même en présence de problèmes XY, cependant, il y aura toujours des situations où cela peut être une préoccupation légitime, mais je préfère les traiter au cas par cas, car, d'après mon expérience, ils sont rare.

Donc, si vous avez un exemple précis de cela, je peux peut-être vous aider, mais je ne pense pas pouvoir répondre de manière significative à la question générale.

@ploeh Merci beaucoup pour cette réponse, qui confirme les approches que j'envisageais (et vous m'avez rendu curieux des cata- et anamorphismes 😃).
Je suis tout à fait d'accord que les valeurs interdépendantes sont principalement un problème XY (au moins dans mon cas), le fait est que lorsque vous travaillez sur du code hérité (non testé 😢), traiter ces valeurs était un bon début pour écrire des tests de toute façon, jusqu'à ce que nous obtenions le temps de refactoriser cela correctement.

Quoi qu'il en soit, vos deux réponses abordent assez bien le problème, je suppose que je suis prêt à partir de là.
Merci!

BTW, j'ai oublié de mentionner que je voulais seulement dire que ma réponse était un ajout à celle de

Je ne l'ai pas pris autrement

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

Questions connexes

ploeh picture ploeh  ·  3Commentaires

Ridermansb picture Ridermansb  ·  4Commentaires

zvirja picture zvirja  ·  8Commentaires

joelleortiz picture joelleortiz  ·  4Commentaires

mjfreelancing picture mjfreelancing  ·  4Commentaires